diff --git a/.strip-manifest b/.strip-manifest index 926fce4e..4e3ee935 100644 --- a/.strip-manifest +++ b/.strip-manifest @@ -164,6 +164,8 @@ sha256 32cb5b2719fd0f50a76a63e92ed0d81f2770d4a260bcc1c096f7d8cec3dc0dc7 pre_bu sha256 fddaf7592554e14867cef501599350affa19d95b408a7c1f4a75b043625f3253 pre_built/el8.x86_64.glibc2p28/bin/stty.bz2 sha256 ea8663a62529908ee65ac75c41adc1874f13c98bd6a2887d9192ddc62810794a pre_built/el8.x86_64.glibc2p28/bin/stylua.bz2 sha256 6d62d65b8dcba1d9330f7e9375af1eddec1933d381917d43853a420df469f55c pre_built/el8.x86_64.glibc2p28/bin/sum.bz2 +sha256 ef1ab201d70c0142e06817393ab229c0923003623aad7d6293afee1816aff32e pre_built/el8.x86_64.glibc2p28/bin/surfer.bin.bz2 +sha256 66570d88c48d7b347f39c21dda2d137908b1b1d2573ae7bd6451c1d2cbe9a241 pre_built/el8.x86_64.glibc2p28/bin/surfer.bz2 sha256 0685493a201147e1bf8e62d9102d39c8a624d9b4837e2593daec4c73e1760f0a pre_built/el8.x86_64.glibc2p28/bin/sync.bz2 sha256 5eacac3388fae845870fa103a742a0bbf83397476ea844085d825f78ba3d21dc pre_built/el8.x86_64.glibc2p28/bin/tac.bz2 sha256 6c68418ef5fa20f1a20693b29f9a631c6be0f4d3f9bcdb74a978be1d98bb2ae4 pre_built/el8.x86_64.glibc2p28/bin/tail.bz2 diff --git a/CLAUDE.md b/CLAUDE.md index cfc435da..0d5ec474 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,6 +147,7 @@ pre_built/ build-octave.sh - Build GNU Octave from source (without Qt/Java/X11; gnuplot backend) build-gvim.sh - Build GTK3 GUI vim (gvim.bin + gvim wrapper script) from source build-nedit-ng.sh - Build nedit-ng Qt5 NEdit rewrite from source (CMake, single binary) + build-surfer.sh - Build Surfer waveform viewer (Rust egui/glow OpenGL GUI) from stable tag; submodule-aware, fresh CARGO_HOME, strip+patchelf+wrapper reproduce-llvm-build.sh - LLVM build reproduction script .strip-manifest - sha256/tar-meta cache for strip_all_elf_binaries @@ -220,7 +221,7 @@ When installer runs, `resolve_tool_selection(args, registry)` performs: 5. Walk soft `recommends` -- silently drop skipped/unknown. 6. Filter by current platform (`linux`/`macos`/`windows`). -Group expansion (`expand_groups`) recursive, cycle-detected. Synthetic groups expand at runtime (not stored in `packages.json`): `@shared` -> every non-group package whose `kind != env` (everything for a shared/read-only tree); `@envs` -> its complement (every `kind == env` config bundle). `all` is also accepted as a name and expands to every non-group package. Tools and env config bundles are **fully decoupled** -- no cross-`recommends` in either direction (a tool never recommends its env bundle, an env bundle never recommends its tool) -- so `./loadout install @shared --dest-dir ...` (shared tree) and `./loadout install @envs` (per-user config) are clean complements needing no `--skip`/`--no-deps`. Both synthetic groups surface in `list --groups` and `describe <@group>` (driven by `_SYNTHETIC_GROUPS`) despite not being registry entries. +Group expansion (`expand_groups`) recursive, cycle-detected. Synthetic groups expand at runtime (not stored in `packages.json`): `@shared` -> every non-group, **non-`optional`** package whose `kind != env` (everything for a shared/read-only tree); `@envs` -> its complement (every `kind == env` config bundle). `all` is also accepted as a name and expands to every non-group, non-`optional` package. **`optional: true` is the only gating field** (there is no `default` field): packages so flagged are skipped by the `all` / `@shared` / `@engineering-loadout` sweeps and install only when named explicitly or pulled by a group that lists them in `members` -- e.g. `surfer` (name-only) and the `@rust` trio (`rust`, `rust-crate-store`, `env-cargo`, reachable via `@rust`). Tools and env config bundles are **fully decoupled** -- no cross-`recommends` in either direction (a tool never recommends its env bundle, an env bundle never recommends its tool) -- so `./loadout install @shared --dest-dir ...` (shared tree) and `./loadout install @envs` (per-user config) are clean complements needing no `--skip`/`--no-deps`. Both synthetic groups surface in `list --groups` and `describe <@group>` (driven by `_SYNTHETIC_GROUPS`) despite not being registry entries. ### Subcommands @@ -240,7 +241,7 @@ Installer uses Click/rich-click subcommands (dnf/apt verbs). Bare `loadout` and ### Selection flags -- Positional `PKG...` -- install exactly these packages/`@groups` (deps still walked unless `--no-deps`). `all` = every non-group package. +- Positional `PKG...` -- install exactly these packages/`@groups` (deps still walked unless `--no-deps`). `all` = every non-group, non-`optional` package. - `--skip NAMES` -- remove packages/`@groups` from install set (`install`, `reinstall`, `upgrade`, `resolve`). - `--no-deps` -- install named set verbatim; no `depends`/`recommends` walk (`install`, `reinstall`, `upgrade`, `resolve`). - `--force` -- continue past resolver errors (e.g. hard dep in skip set), printing `WARNING:` row instead of erroring (`install`, `reinstall`, `upgrade`, `resolve`). @@ -293,6 +294,8 @@ Each phase installer (`install_prebuilt_binaries`, `install_fonts`, `install_tld **VCD toggle profiler runtime behavior**: `vcd-toggle-profiler` is a C++17 runtime package built from `github.com/smprather/vcd-toggle-profiler` (C++ implementation only). The archive `pre_built//runtime/vcd-toggle-profiler.tar.bz2` extracts to `~/.local/` and installs `bin/vcd-toggle-profiler` (POSIX-sh wrapper), `lib/vcd-toggle-profiler/vcd-toggle-profiler.bin` (real ELF), `share/vcd-toggle-profiler/uplot/{uPlot.iife.js,uPlot.min.css}`, and license files. The wrapper derives its prefix from its installed path and supplies `--uplot-js/--uplot-css` defaults so report generation works from any current directory while still allowing explicit user overrides. Build with `pre_built/build_scripts/build-vcd-toggle-profiler.sh --rev ` or `--source `; it deliberately uses EL8 system `/usr/bin/g++` 8.5 with `-lstdc++fs`, not upstream CMake Release (`-march=native`), keeping the deployed binary portable across farm CPUs and capped at `GLIBCXX_3.4.21`. `pigz` is recommended so `.vcd.gz` input uses the bundled fast gzip path when available. Re-run `./loadout completion bash > envs/bash/global/completions/loadout.bash` after changing the package registry. +**Surfer behavior**: `surfer` is an `optional` (name-only) `bin` package -- a Rust egui/**glow (OpenGL)** waveform viewer (VCD/FST/GHW). Built from the latest stable tag on EL8 by `pre_built/build_scripts/build-surfer.sh --tag vX.Y.Z` (native glibc-2.28; release profile has no `-march=native`). Packaged gvim-style: `bin/surfer.bz2` (POSIX-sh wrapper) + `bin/surfer.bin.bz2` (stripped ELF, RPATH `$ORIGIN/../lib64`). The wrapper mirrors wezterm's GL block (prepends `/lib64` to `LD_LIBRARY_PATH`, sets `LIBGL_DRIVERS_PATH`/`__EGL_VENDOR_LIBRARY_DIRS`) before exec'ing `surfer.bin`. `depends: [gui_libs, mesa3d_libs]`; the ELF NEEDs only glibc + libgcc_s, winit/glow **dlopen** libGL/X11/wayland/xkbcommon at runtime (GLVND dispatcher stays host-provided, never bundled). Install: `./loadout install surfer` (auto-pulls gui_libs + mesa3d_libs). `surfer server` is a built-in headless mode. Build quirks (in `ADDING_BINARIES.md`): the v0.7.0 tag vendors f128 + instruction-decoder as **git submodules** (clone `--recurse-submodules`); the loadout's offline `~/.cargo/config.toml` registry-store must be bypassed with a fresh `CARGO_HOME`. + **GObject typelib behavior**: Installer copies `*.typelib` files from `pre_built//typelibs/` to `~/.local/lib/girepository-1.0/`. `envs/bash/global/bashrc` exports `GI_TYPELIB_PATH=$HOME/.local/lib/girepository-1.0` when that dir exists, allowing Python tools using `import gi` (PyGObject) to find bundled typelibs. Required typelibs documented in `"typelibs"` key of `packages.json` entries (reference only -- installer copies all typelibs unconditionally). Typelib files from EL8 RPMs: `gobject-introspection` (GLib/GObject/Gio/GIRepository), `gtk3` (Gtk/Gdk/GdkPixbuf), `gtksourceview3` (GtkSource-3.0). Plain files, no strip/patchelf needed. **Meld runtime behavior (shanghai bundle)**: Meld 3.20.4 bundled as "shanghai" -- extracted from system-installed EL8 package, repacked into `pre_built//runtime/meld.tar.bz2`. Uses **system Python 3.6** (`/usr/bin/python3.6`) with bundled PyGObject 3.28.3 (gi) and pycairo 1.16.3. Bundle extracts to `~/.local/` and installs: `lib/python3.6/site-packages/{gi,cairo,meld,meld3}/`, `share/meld/`, `bin/meld` (Python 3.6 launcher). Launcher derives its local prefix from installed `bin/meld`, exports `LOADOUT_LOCAL_PREFIX`, and sets `sys.path`, `GI_TYPELIB_PATH`, `LD_LIBRARY_PATH` before `import gi` -- `LD_LIBRARY_PATH` must be set before `import gi` so `dlopen()` for `_gi.cpython-36m.so` finds installed `lib64/libgirepository-1.0.so.1`. Bundled `meld/conf.py` has DATADIR patched to `LOADOUT_LOCAL_PREFIX/share/meld` (was `/usr/share/meld`; fallback remains `~/.local/share/meld` only for direct import outside the launcher). Exclusively-owned libs: `libgirepository-1.0.so.1`, `libgtksourceview-3.0.so.1` (both patchelf'd RPATH `$ORIGIN`). Installer function: `install_meld_runtime()`. Typically installed with gui_libs: `./loadout install gui_libs,meld`. Installer warns if meld selected without gui_libs (GTK3/Qt5/X11 libs all from gui_libs). To update meld: re-`yum install meld` on EL8 build machine, re-run shanghai extract steps (copy gi/cairo/meld packages + data files into bundle dir, patch conf.py, create tar.bz2, bzip2 new libs if version changed, update packages.json version), run `./strip_all_elf_binaries`, commit. **Why shanghai instead of PyPI wheels**: PyGObject has no binary wheels on PyPI (source-only, requires gobject-introspection headers). EL8 ships GLib 2.56.4; PyGObject >= 3.36 requires GLib >= 2.62; PyGObject <= 3.30 (works with GLib 2.56) incompatible with Python 3.14 (`Py_TYPE()` assignment removed). System Python 3.6 sidesteps all of this. @@ -309,6 +312,8 @@ Each phase installer (`install_prebuilt_binaries`, `install_fonts`, `install_tld **Rolling-git wheel behavior**: First-party `python-tool` packages can be tagged `"rolling_git": ""` in `packages.json` (currently `liberty-tools`, `text-serdes`, `time-plot`). `./update ` (and bare `./update`, which now includes all rolling pkgs) rebuilds these from source instead of using a hand-bundled wheel: cheap `git ls-remote` skip-check -> fresh `git clone --filter=blob:none` (keeps tags so `describe` works; falls back to full clone) -> `uv build --wheel` (handles maturin **and** hatchling backends) -> prune previous `-*.whl` -> copy new wheel into `pre_built//wheels/` -> surgically stamp the package's `version` to `git describe --tags --always --dirty`. Rebuild happens **only when the source commit changed**; `./update --rebuild` forces. `./update --list[-outdated]` shows rolling rows with latest = remote HEAD sha. **maturin gotcha:** under PEP517 maturin tags a bare `linux_x86_64` wheel; `update` sets `MATURIN_PEP517_ARGS="--compatibility manylinux_2_28"` for maturin backends so the wheel is portable + auditwheel-checked. Builds need network + `~/.cache/uv` write (run on the EL8 build machine). Add a 4th rolling project by adding the `rolling_git` field -- `./update` discovers it automatically. To add a new third-party (pinned) Python tool instead, see *Python tool behavior* below. +**cicwave Python tool behavior**: `cicwave` optional `uv_tool` (`optional: true` -- opt in with `./loadout install cicwave`; not in any bulk group). PyQtGraph waveform viewer (ngspice/Xyce/VCD/CSV). It is a **loadout PyQt6 fork** of the upstream PySide6 app: PySide6 has **no wheel that is both EL8 (glibc 2.28) and Python 3.14** -- 6.9.x is `manylinux_2_28` but `python<3.14`; 6.10+ added 3.14 but jumped to `manylinux_2_34` (glibc 2.34/RHEL9, a one-way ratchet). PyQt6 satisfies both (`PyQt6 6.9.1` abi3 cp39, `PyQt6-Qt6 6.9.2` `manylinux_2_28` with `libQt6Core` floor `GLIBC_2.28`, `PyQt6-sip` cp314). The fork lives as `pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch` (PySide6->PyQt6 imports, `pyqtSignal as Signal`, **25 strict-enum scopings** like `Qt.AlignCenter -> Qt.AlignmentFlag.AlignCenter`, two dynamic `getattr(QPalette.ColorRole, ...)` / `ShortcutContext` fixes, and the pyproject `PySide6 -> PyQt6` dep so the wheel resolves offline). Build/bundle with `pre_built/build_scripts/build-cicwave.sh --tag 0.5.2` (clones stable tag, applies patch, `uv build`, downloads the PyQt6 + matplotlib closure as EL8/cp314 wheels, splits the ~79 MB `pyqt6_qt6` wheel into `.whl.part-NNN` -- the installer's `_prepare_wheels_dir` rejoins it like the polars/pyarrow wheels). numpy/pandas/click/pyyaml/packaging/python_dateutil/six are reused from the existing bundle. Headless-verify with `QT_QPA_PLATFORM=offscreen` + `CICSIM_USE_OPENGL=0` (offscreen Qt has no GL); interactive paths need a real X/WSLg smoke after a version bump. See `ADDING_BINARIES.md` -> "cicwave". + **JupyterLab Python tool behavior**: `jupyterlab` optional `uv_tool` (`optional: true` -- opt in with `./loadout install jupyterlab`). Installed via `uv tool install jupyterlab` using bundled wheels. After install, `jupyter` and `jupyter-lab` launchers at `~/.local/bin/`. Users run `jupyter lab`; JupyterLab opens in system browser. Requires accessible browser (WSL2: Windows browser via WSL interop; headless farm nodes: point `BROWSER` to VNC-accessible browser or use `--no-browser --port=8888` with port forwarding). Wheels must be downloaded with `PIP_REQUIRE_VIRTUALENV=0 pip3.14 download jupyterlab --platform manylinux_2_28_x86_64 --python-version 3.14 --only-binary :all: -d pre_built//wheels/` -- JupyterLab has ~80 dependency packages. **Backup behavior**: Numbered backups in `loadout_backups/backup.N/` (always starts at `.1`; never bare `backup`). Skips files already pointing to repo. Never overwrites existing backups. At end of successful install, backup dir compressed to `loadout_backups/backup.N.tar.bz2`, uncompressed dir removed; numbering checks both `backup.N/` and `backup.N.tar.bz2` when picking next N. Post-install hooks (receive `LOADOUT_BACKUP_DIR`) run before compression. `./loadout snapshot restore ` accepts uncompressed dirs or `.tar.bz2` archives (extracts to `/tmp`, restores). Main backups exclude font files (`*.ttf`, `*.otf`, `*.pcf`, `*.bdf`, `*.woff`, `*.woff2`, etc.) -- large and reproducible. The fonts phase has its own `fonts.bak*` move for normal installs and suppresses it under `--no-backup`. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 7c86182e..83923906 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,9 +1,43 @@ # Current Handoff -Last updated: 2026-06-27. +Last updated: 2026-06-28. ## Recent Integration +`surfer` v0.7.0 (`gitlab.com/surfer-project/surfer`) added -- a Rust egui/glow +(OpenGL) waveform viewer (VCD/FST/GHW). Built from the **v0.7.0 tag** on EL8 +(native glibc-2.28) by `pre_built/build_scripts/build-surfer.sh`. Packaged as a +gvim-style `bin` pair: `pre_built//bin/surfer.bin.bz2` (stripped ELF, +RPATH `$ORIGIN/../lib64`) + `surfer.bz2` (POSIX-sh wrapper with the wezterm +Mesa/GLVND env block). `kind: bin`, `optional: true`, `depends: [gui_libs, +mesa3d_libs]`. The ELF NEEDs only glibc base + libgcc_s; winit/glow dlopen +libGL/X11/wayland/xkbcommon at runtime (gui_libs + mesa3d_libs cover them, GLVND +dispatcher stays host). Build gotchas captured in `ADDING_BINARIES.md`: the +v0.7.0 tag vendors f128 + instruction-decoder as **git submodules** (main later +switched to `git =` deps), and the loadout's own `~/.cargo/config.toml` offline +registry-store must be bypassed with a fresh `CARGO_HOME`. + +**Resolver change (opt-in tightening):** the synthetic `all` group -- and +`@shared`, which builds on the same sweep -- now **skip `optional: true`** +packages (`loadout_main.py` `expand_groups`). Effect: surfer is name-only +(install via `./loadout install surfer`), kept OUT of `all` / `@shared` / +`@engineering-loadout`. Side effect (intended): the `@rust` trio (`rust`, +`rust-crate-store`, `env-cargo`, all `optional: true`) likewise drops from the +full bundle -- still installable via `@rust`. Nothing `depends` on `rust`, so no +fallout. The release smoke (`test-prebuilt-binaries` installs `@shared`) no +longer drags rust; rust keeps its own `test-rust-offline-almalinux8` gate. + +**Stale-doc purge:** the registry has **0** `default: true` packages -- the +`default` field is gone and the resolver never read it (`optional: true` is the +only gating field). Removed lingering `default: true` / "default set" references +from `ADDING_BINARIES.md` (cloc/scc/tokei notes + the new-binary template). + +**Known pre-existing gap (NOT surfer):** `pre_built//bin/models.bz2` +ships with no `models` entry in `packages.json`, so it is an *unregistered* bin +stem -- `install_prebuilt_binaries._bin_selected` always installs unregistered +stems, so every single-tool install drags `models` in. Surface for a separate +fix (add a `models` package or drop the orphan `.bz2`). + AutoHotkey runtime-config refactor was integrated from `0001-ahk-read-feature-config-from-loadout_keys.toml-at-ru.patch`. `envs/autohotkey/hotkeys.ahk` now reads `%USERPROFILE%\loadout_keys.toml` at startup @@ -70,6 +104,30 @@ wrapper, real ELF, uPlot assets, and licenses. Build script: These checks passed after the recent changes: +- Passed for surfer: `cargo build --release -p surfer` from the v0.7.0 tag + (submodules + fresh `CARGO_HOME`) -> `surfer 0.7.0 (git: v0.7.0)`, glibc + symbol floor `GLIBC_2.28`, `ldd` host-libs-only; staged the bz2 pair and + `strip_all_elf_binaries` recorded both (ELF skipped as patchelf'd, wrapper as + non-ELF); `./loadout install surfer --no-deps --dest-dir ` then the + installed wrapper ran `surfer --version`; `bash -n` on the wrapper + + `build-surfer.sh`; resolver assertions (surfer + `@rust` trio absent from + `all`/`@shared`/`@engineering-loadout`, surfer reachable by name, `@rust` + intact); `./loadout doctor` exit 0, `./loadout info surfer`, + `./loadout list --tag waveform`; `py_compile` of `loadout_main.py` + + `farm-versions`; `json.tool` of `packages.json`; regenerated bash completion + (`bash -n` clean); `git diff --check`. +- Passed for cicwave (PyQt6 fork): patch applies cleanly on a fresh `0.5.2` tag + clone (`git apply --check`); `uv build` produced the wheel (METADATA now + `Requires-Dist: PyQt6`); offline `uv tool install` against the real bundle + `wheels/` resolved the full PyQt6 closure reusing existing numpy 2.4.6 / + pandas 3.0.3; `pyqt6_qt6` split into `.whl.part-000/001` and + `_prepare_wheels_dir` rejoined byte-identical; **full** `./loadout install + cicwave --dest-dir --no-backup` succeeded (portable Python + rejoin + uv + tool install); installed `cicwave --help` + a headless offscreen render + (window, 3 plotted waves, matplotlib PNG export) with no enum/API errors; + `json.tool` of `packages.json`, resolver name-only/optional assertions, + `./loadout doctor` exit 0, `bash -n` completion, `bash -n build-cicwave.sh`. + NOT smoked on a real display. - Passed for this Markdown sync: `git diff --check`, `sh -n loadout`, `python3 -m py_compile loadout_main.py`, and `bash -n envs/bash/global/bashrc`. - Passed for the AutoHotkey runtime-config patch integration: `git diff @@ -156,3 +214,32 @@ exec bash - `hotkeys.ahk` reads `loadout_keys.toml` at startup (reusing its partial TOML parser); the installer no longer stamps feature booleans. Override the config path with `LOADOUT_KEYS_TOML`; `--print-config` dumps the resolved config. + +## cicwave (integrated as a PyQt6 fork) + +- **cicwave** 0.5.2 (`github.com/wulffern/cicwave`) -- a PyQtGraph waveform + viewer -- is now bundled as an `optional` `python-tool` (`uv_tool: cicwave`, + name-only). The earlier "wait for PySide6 3.14" plan was **wrong**: PySide6 + 6.10 added Python 3.14 *and simultaneously* raised its linux wheel floor to + `manylinux_2_34` (glibc 2.34 / RHEL9; `QtCore.abi3.so` floor `GLIBC_2.34`, + empirically verified). EL8 is glibc 2.28, so no 3.14-capable PySide6 wheel will + ever run here -- a one-way ratchet, not a wait. **PyQt6** does satisfy both + (PyQt6-Qt6 6.9.2 is `manylinux_2_28`, `libQt6Core` floor `GLIBC_2.28`; binding + abi3 cp39; sip cp314), so we carry a small fork. +- The port is `pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch` + (one file `wave_pg.py` + `pyproject.toml`, 56 +/- lines): PySide6->PyQt6 + imports, `pyqtSignal as Signal`, 25 strict-enum scopings, two dynamic + `getattr(QPalette.ColorRole, ...)` / `ShortcutContext` fixes, and the pyproject + `PySide6 -> PyQt6` dependency. Built/bundled by + `pre_built/build_scripts/build-cicwave.sh --tag 0.5.2`. The ~79 MB `pyqt6_qt6` + wheel is split into `.whl.part-NNN` (installer `_prepare_wheels_dir` rejoins, + like polars/pyarrow); numpy/pandas/click/pyyaml/etc reused from the bundle. +- Verified end-to-end: `./loadout install cicwave --dest-dir --no-backup` + on this EL8 box extracted portable Python, rejoined the Qt wheel, ran + `uv tool install cicwave` offline, installed `cicwave`/`cicwavew`; the + installed tool then rendered a real PNG headless + (`QT_QPA_PLATFORM=offscreen CICSIM_USE_OPENGL=0`, window + 3 plotted waves + + matplotlib export, no enum errors). **Not** yet smoked on a real X/WSLg display + -- interactive-only enum paths (drag/drop, context menus, key modifiers) are + statically scoped but unexercised headlessly; smoke on a display before relying + on the GUI, and after any version bump. diff --git a/envs/bash/global/completions/loadout.bash b/envs/bash/global/completions/loadout.bash index 93244552..de9e0a12 100644 --- a/envs/bash/global/completions/loadout.bash +++ b/envs/bash/global/completions/loadout.bash @@ -20,7 +20,7 @@ _loadout_complete() { cword=$COMP_CWORD local verbs="install reinstall upgrade update list search info describe resolve doctor snapshot clean completion" - local pkgs="agent-deck amux bash bash-docs bat biome broot btm btop bzip2 choose clang cloc dasel delta duf dust env-autohotkey env-bash env-cargo env-editorconfig env-helix env-nvim env-pip env-powershell env-starship env-tmux env-vim env-wezterm env-zsh expect eza fd fio firefox fish flameshot font-cascadiacode font-dejavusansmono font-envycoder font-firacode font-hack font-inconsolata font-iosevkaterm font-jetbrainsmono font-meslolg font-robotomono font-sourcecodepro font-ubuntumono fresh fzf glow gnu-coreutils gnuplot gobject-typelibs gocheat gopls gping gui_libs gvim htop hx hyperfine jq jupyterlab just keyb lazygit lefdef-tools liberty-filter liberty-tools llvm-bolt lua-language-server mate-terminal meld mesa3d_libs micro miller modules ncdu nedit-ng ngspice ninja nodejs numr nvim nvim-catalog-plugins nvim-qt octave p7zip patchelf pdftotext pigz portable-python procs pv pygwalker pyright rg rsync ruff rust rust-analyzer rust-crate-store scc sd shellcheck shfmt st starship stylua tcl text-serdes time-plot tk tkdiff tldr tldr-data tmux tmux-path-store tokei tree-sitter treesitter-parsers ty urxvt uv vcd-toggle-profiler vim visidata wezterm xsel xterm yank yara yazi yq zellij zoxide zsh" + local pkgs="agent-deck amux bash bash-docs bat biome broot btm btop bzip2 choose cicwave clang cloc dasel delta duf dust env-autohotkey env-bash env-cargo env-editorconfig env-helix env-nvim env-pip env-powershell env-starship env-tmux env-vim env-wezterm env-zsh expect eza fd fio firefox fish flameshot font-cascadiacode font-dejavusansmono font-envycoder font-firacode font-hack font-inconsolata font-iosevkaterm font-jetbrainsmono font-meslolg font-robotomono font-sourcecodepro font-ubuntumono fresh fzf glow gnu-coreutils gnuplot gobject-typelibs gocheat gopls gping gui_libs gvim htop hx hyperfine jq jupyterlab just keyb lazygit lefdef-tools liberty-filter liberty-tools llvm-bolt lua-language-server mate-terminal meld mesa3d_libs micro miller modules ncdu nedit-ng ngspice ninja nodejs numr nvim nvim-catalog-plugins nvim-qt octave p7zip patchelf pdftotext pigz portable-python procs pv pygwalker pyright rg rsync ruff rust rust-analyzer rust-crate-store scc sd shellcheck shfmt st starship stylua surfer tcl text-serdes time-plot tk tkdiff tldr tldr-data tmux tmux-path-store tokei tree-sitter treesitter-parsers ty urxvt uv vcd-toggle-profiler vim visidata wezterm xsel xterm yank yara yazi yq zellij zoxide zsh" local groups="@all-environments @compression @core-cli @core-environments @data-bundles @dev-tools @editor-cli @editor-gui @editor-suite @engineering-loadout @envs @fonts-all @fonts-default @gui-suite @python-stack @python-tools-extra @rust @scientific @security @shared @shells-optional @system-monitor all" local tags="agent automation benchmark browser build cache clipboard compat compiler compress core data def dev diff disk docs document eda editor elf env file font format git gobject gui hotkeys hpc javascript lang lef liberty library lint llvm lsp lua markdown math monitor multiplexer nav network notebook nvim opengl optimizer parser pkg plot profiler prompt python reference runtime rust scientific screenshot scripting search security shell simulator spice sync system terminal text tmux toolchain tui vcd viewer visualization waveform" diff --git a/loadout_main.py b/loadout_main.py index 40162f33..baf387c7 100755 --- a/loadout_main.py +++ b/loadout_main.py @@ -238,7 +238,7 @@ def _parse_namelist(raw): # expand_groups() special-cases each name below; the descriptions here drive # `list --groups` and `describe`. _SYNTHETIC_GROUPS = { - "@shared": "Every non-env package -- install set for a shared/read-only tree.", + "@shared": "Every non-env, non-optional package -- install set for a shared/read-only tree.", "@envs": "Every per-user env config bundle (the complement of @shared).", } @@ -250,7 +250,10 @@ def expand_groups(names, registry, _stack=None): bundles -- i.e. everything you install into a shared/read-only tree. @envs is its complement (every "env" package); pair them as `--only @shared --skip @envs` to keep env recommends out of a shared tree. - Cycles in the group graph raise ResolverError. + `all` (and the @shared / @engineering-loadout sweeps that build on it) skip + packages flagged "optional": true -- those install only when named explicitly + or pulled in by a group that lists them (e.g. surfer, the @rust trio). Cycles + in the group graph raise ResolverError. Unknown package names produce a warning and are dropped. """ if _stack is None: @@ -258,10 +261,14 @@ def expand_groups(names, registry, _stack=None): out = set() for name in names: if name == "all": - out |= {n for n in registry if not n.startswith("@")} + out |= {n for n, e in registry.items() if not n.startswith("@") and not e.get("optional")} continue if name == "@shared": - out |= {n for n, e in registry.items() if not n.startswith("@") and e.get("kind") != "env"} + out |= { + n + for n, e in registry.items() + if not n.startswith("@") and e.get("kind") != "env" and not e.get("optional") + } continue if name == "@envs": out |= {n for n, e in registry.items() if not n.startswith("@") and e.get("kind") == "env"} diff --git a/pre_built/ADDING_BINARIES.md b/pre_built/ADDING_BINARIES.md index 0e404b95..b5f43b42 100644 --- a/pre_built/ADDING_BINARIES.md +++ b/pre_built/ADDING_BINARIES.md @@ -191,10 +191,15 @@ Key rules: - `recommends` -- list of soft-dep package names. Silently dropped if skipped. For a tool with a single binary, no exclusive libs, and no deps: -`"mytool": {"kind": "bin", "bins": ["mytool"], "version": "X.Y.Z", "platforms": ["linux"], "default": true, "description": "..."}`. +`"mytool": {"kind": "bin", "bins": ["mytool"], "version": "X.Y.Z", "platforms": ["linux"], "description": "..."}`. -To make it discoverable by group selection, add it to an `@group` `members` list elsewhere -in `packages.json` (e.g. `@core-cli`, `@dev-tools`, `@editor-cli`). +There is no `default` field -- the registry is pure opt-in (bare `install` errors). A +plain non-`optional` package is swept into the synthetic `all` group, so it ships in the +full `@engineering-loadout` bundle automatically. Add it to an `@group` `members` list +elsewhere in `packages.json` (e.g. `@core-cli`, `@dev-tools`, `@editor-cli`) to make it +discoverable by group selection. Set `"optional": true` to keep it OUT of `all` / +`@shared` / `@engineering-loadout` so it installs only when named explicitly or pulled by +a group that lists it (e.g. `surfer`, the `@rust` trio). ### 8. Verify and commit @@ -1073,11 +1078,11 @@ chmod 644 pre_built/el8.x86_64.glibc2p28/bin/cloc.bz2 - No patchelf, no bundled libs, no RPATH -- it's a script. `strip_all_elf_binaries` decompresses, sees a non-ELF payload, records the sha in `.strip-manifest`, and skips it on later runs (same handling as the `vim.bz2` shell wrapper). -- packages.json: `kind: bin`, `default: true`, `tags: ["dev","data"]`, no `libs`. +- packages.json: `kind: bin`, `tags: ["dev","data"]`, no `libs`. - farm-versions: `strategy_flag(["--version"], r"([0-9]+\.[0-9]+)")` (cloc prints a bare two-part version). check-versions resolves latest from the GitHub homepage. -Install: `./loadout install cloc` (or it's in the default set). +Install: `./loadout install cloc` (also swept into the full `@engineering-loadout` bundle). --- @@ -1097,7 +1102,7 @@ chmod 644 pre_built/el8.x86_64.glibc2p28/bin/scc.bz2 ./strip_all_elf_binaries ``` -packages.json `kind: bin`, `default: true`, `tags: [dev,data]`, no libs. +packages.json `kind: bin`, `tags: [dev,data]`, no libs. farm-versions: `strategy_flag(["--version"], r"scc version ([0-9]+\.[0-9]+\.[0-9]+)")`. ## tokei 14.0.0 -- code counter (Rust, EL8 SOURCE build) @@ -1124,11 +1129,11 @@ chmod 644 pre_built/el8.x86_64.glibc2p28/bin/tokei.bz2 ``` - System libs only -> no bundling, no RPATH. Max glibc 2.28 (native EL8 build). -- packages.json `kind: bin`, `default: true`, `tags: [dev,data]`, no libs. +- packages.json `kind: bin`, `tags: [dev,data]`, no libs. - farm-versions: `strategy_flag(["--version"], r"tokei ([0-9]+\.[0-9]+\.[0-9]+)")`. - When tokei resumes shipping prebuilts (>v14) or v14 gets binaries, a download is fine. -Install both: `./loadout install scc,tokei` (both in the default set). +Install both: `./loadout install scc,tokei` (both swept into the full `@engineering-loadout` bundle). --- @@ -1853,3 +1858,138 @@ registry change. **Verify:** `ldd lib/vcd-toggle-profiler/vcd-toggle-profiler.bin` (host libs only), symbol-version floor `GLIBC <= 2.14` / `GLIBCXX <= 3.4.21`, and a wrapper run against a sample VCD producing an HTML report. + +--- + +## surfer v0.7.0 -- waveform viewer (Rust egui/glow OpenGL GUI, EL8 SOURCE build) + +Surfer (`gitlab.com/surfer-project/surfer`) is a VCD/FST/GHW waveform viewer for +digital hardware debugging, written in Rust on `eframe` with the **glow** +(OpenGL) backend + winit (x11 + wayland). Upstream prebuilts target newer glibc, +so we build the latest stable tag from source on EL8 -> native glibc-2.28 +binary. Packaged as a `bin`-package pair like gvim (wrapper + real ELF), `optional` +so it stays out of the full `@engineering-loadout` bundle. + +```bash +pre_built/build_scripts/build-surfer.sh --tag v0.7.0 +./strip_all_elf_binaries +./loadout completion bash > envs/bash/global/completions/loadout.bash +``` + +**Prerequisites / quirks (all handled by the build script):** +- **Toolchain:** repo's bundled rust (`cargo 1.96`); surfer MSRV is 1.92. The + f128 submodule's `__float128` shim wants a quadmath-capable C compiler, so the + script enables `gcc-toolset-14`. glibc floor stays 2.28 regardless of gcc. +- **Submodules:** the **v0.7.0 tag** vendors `f128` and `instruction-decoder` as + git submodules (path deps); `main` later switched them to `git =` deps. Clone + with `--recurse-submodules` or the workspace fails to resolve `f128`. +- **Offline crate-store trap:** the loadout's own `~/.cargo/config.toml` + (env-cargo) replaces crates-io with the offline `registry-store`, which only + holds the curated crate subset and lacks surfer's pins (e.g. `camino 1.2.1`). + The script sets a fresh `CARGO_HOME` so the build hits real crates.io. +- **Build:** `cargo build --release -p surfer`; release profile is `opt-level=3 + lto=true` with **no** `-march/target-cpu=native`, so the ~47 MB binary is + farm-portable. Pulls extism/wasmtime + reqwest/rustls (plugin + online + features) -> ~700 crates; first build is slow, LTO link dominates. + +**Packaging:** strip, `patchelf --set-rpath '$ORIGIN/../lib64'` (Mesa vendor libs +sit one level up from `bin/`), then bzip2 both the stripped ELF (`surfer.bin.bz2`) +and the POSIX-sh wrapper (`surfer.bz2`) into `pre_built//bin/`. The +wrapper mirrors wezterm's GL block: derive prefix from `bin/..`, prepend +`/lib64` to `LD_LIBRARY_PATH`, set `LIBGL_DRIVERS_PATH=/lib64/dri` +and `__EGL_VENDOR_LIBRARY_DIRS=/share/glvnd/egl_vendor.d`, then exec +`surfer.bin`. packages.json `kind: bin`, `bins: [surfer, surfer.bin]`, +`optional: true`, `depends: [gui_libs, mesa3d_libs]`, tags `gui/opengl/waveform/eda`. + +**Runtime libs:** the ELF itself NEEDs only glibc base + `libgcc_s` -- winit/glow +**dlopen** everything else at runtime (`libGL.so.1`, `libX11`, `libwayland-client`, +`libxkbcommon`, ...). X11/Wayland/xkbcommon come from `gui_libs`; the Mesa vendor +side (software `swrast`/`llvmpipe` for headless farm nodes) from `mesa3d_libs`; +the GLVND dispatcher `libGL.so.1` stays host-provided (never bundled). Install: +`./loadout install surfer` (auto-pulls gui_libs + mesa3d_libs). + +**Verify:** `readelf -V surfer.bin` glibc floor `<= 2.28`, `ldd` shows host libs +only, and a real-X/WSLg `surfer --version` + a headed launch on a sample +`.vcd`/`.fst`. + +--- + +## cicwave 0.5.2 -- PyQtGraph waveform viewer (Python uv_tool, loadout PyQt6 fork) + +cicwave (`github.com/wulffern/cicwave`) is a pure-Python PyQtGraph waveform +viewer (ngspice `.raw` / Xyce `.prn` / VCD / CSV / parquet ...). Upstream imports +**PySide6**, which has **no wheel that is both EL8 (glibc 2.28) and Python 3.14**: + +| PySide6 | python | manylinux | EL8 (glibc 2.28)? | +|---------|--------|-----------|-------------------| +| 6.9.3 | `<3.14`| 2_28 | yes, but not 3.14 | +| 6.10.0+ | 3.14 ok| **2_34** | **no** (needs glibc 2.34 / RHEL9; `QtCore.abi3.so` floor `GLIBC_2.34`) | + +6.10 added 3.14 support *and* bumped the glibc floor in the same release -- a +one-way ratchet, so "wait for PySide6" never helps EL8. **PyQt6** does satisfy +both: `PyQt6 6.9.1` is `cp39-abi3` (runs on 3.14), `PyQt6-Qt6 6.9.2` is +`manylinux_2_28` (`libQt6Core.so` floor **GLIBC_2.28**), `PyQt6-sip 13.11.1` is a +`cp314` wheel on ancient manylinux. So we carry a small fork. + +### The PyQt6 port patch (`build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch`) + +Touches only `wave_pg.py` + `pyproject.toml` (56 +/- lines). Upstream is shiboken +(PySide6), which tolerates unscoped enums; PyQt6 is sip, which **removed** them. +The patch is fully reproducible: + +1. **Imports:** `from PySide6.* -> from PyQt6.*`, `from PySide6 import QtCore -> + from PyQt6 import QtCore`. +2. **Signal:** `from PyQt6.QtCore import Qt, pyqtSignal as Signal, ...` (alias + keeps the 7 `xxx = Signal(...)` class-attr definitions untouched). +3. **Strict enums (25 distinct tokens, ~58 sites):** scope every one, e.g. + `Qt.AlignCenter -> Qt.AlignmentFlag.AlignCenter`, + `Qt.UserRole -> Qt.ItemDataRole.UserRole`, + `QHeaderView.Stretch -> QHeaderView.ResizeMode.Stretch`, + `QEvent.Drop -> QEvent.Type.Drop`, + `QFontDatabase.FixedFont -> QFontDatabase.SystemFont.FixedFont`, + `QPalette.Text -> QPalette.ColorRole.Text`, `QDialog.Accepted -> + QDialog.DialogCode.Accepted`, ... +4. **Two dynamic enum lookups** sed cannot see statically: + `getattr(QPalette, role_name) -> getattr(QPalette.ColorRole, role_name)` (theme + palette) and `_Qt.ApplicationShortcut -> _Qt.ShortcutContext.ApplicationShortcut`. +5. **pyproject dependency** `PySide6 -> PyQt6` (else the built wheel still + declares PySide6 and `uv tool install` cannot resolve offline). + +### Build + bundle + +```bash +pre_built/build_scripts/build-cicwave.sh --tag 0.5.2 +./loadout completion bash > envs/bash/global/completions/loadout.bash +``` + +The script clones the **stable tag**, applies the patch, `uv build`s the wheel +into `wheels/`, downloads the PyQt6 + matplotlib dependency closure as EL8/cp314 +wheels, and `split`s any wheel over 40 MiB into `.whl.part-NNN` +(`pyqt6_qt6` is ~79 MB -> 2 parts; the installer's `_prepare_wheels_dir` rejoins +them before `uv tool install`, same as the polars/pyarrow wheels). +numpy/pandas/click/pyyaml/packaging/python_dateutil/six are **reused** from the +existing bundle, not re-fetched. packages.json: `kind: python-tool`, +`uv_tool: cicwave`, `optional: true`, `depends: [portable-python, uv]`. + +### Verify (headless) + +The GUI can't open in CI, but `QT_QPA_PLATFORM=offscreen` constructs real Qt +widgets, so the enum/API port is exercised by rendering to a file (Qt offscreen +has no GL, so force the raster path with `CICSIM_USE_OPENGL=0`): + +```bash +./loadout install cicwave --dest-dir /tmp/t --no-backup # offline, rejoins + uv tool install +QT_QPA_PLATFORM=offscreen /tmp/t/local/bin/cicwave --help +# construct window + plot waves + matplotlib export (exercises pen-style/palette enums): +QT_QPA_PLATFORM=offscreen CICSIM_USE_OPENGL=0 /bin/python -c \ + "from cicwave.wave_pg import CmdWavePg; c=CmdWavePg('time'); c.openFile('s.csv'); \ + c.win._plot_all_visible_waves(); c.exportAndExit('out.png')" +``` + +Interactive-only paths (drag/drop `QEvent.Type.*`, context menus, keyboard +modifiers) are statically scoped but not headlessly exercised -- smoke them on a +real X/WSLg display after a version bump. **Updating:** re-run +`build-cicwave.sh --tag `; if upstream restructures `wave_pg.py` the patch +may need refreshing (`git apply --reject`, fix `.rej`, regenerate). When PySide6 +ships a manylinux_2_28 + python>=3.14 wheel (unlikely; the floor only rises), the +fork could be dropped for a stock PySide6 uv_tool. diff --git a/pre_built/build_scripts/build-cicwave.sh b/pre_built/build_scripts/build-cicwave.sh new file mode 100755 index 00000000..083bcc66 --- /dev/null +++ b/pre_built/build_scripts/build-cicwave.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Build the cicwave wheel + bundle its PyQt6 dependency wheels for EL8 / Python 3.14. +# +# cicwave (github.com/wulffern/cicwave) is a pure-Python PyQtGraph waveform +# viewer that imports PySide6. PySide6 has NO wheel that is both EL8-compatible +# (glibc 2.28) AND Python-3.14-capable: 6.9.x is manylinux_2_28 but caps at +# python<3.14; 6.10+ added 3.14 but jumped to manylinux_2_34 (glibc 2.34, +# RHEL9+). PyQt6, by contrast, ships an abi3 cp39 binding + a manylinux_2_28 +# Qt6 (libQt6Core floor GLIBC_2.28) + a cp314 sip -- all EL8 + 3.14. +# +# So we carry a small fork: 0001-port-pyside6-to-pyqt6.patch rewrites the +# PySide6 imports to PyQt6, aliases pyqtSignal->Signal, scopes every enum to +# PyQt6's strict form (Qt.AlignCenter -> Qt.AlignmentFlag.AlignCenter, ...), +# fixes the one dynamic getattr(QPalette, role) -> getattr(QPalette.ColorRole, +# role), and swaps the pyproject dependency PySide6 -> PyQt6. +# +# This script clones the stable tag, applies the patch, `uv build`s the wheel, +# downloads the PyQt6 + matplotlib dependency closure as EL8/3.14 wheels, and +# chunks any wheel over the GitHub-safe size. numpy/pandas/click/pyyaml and +# other already-bundled shared wheels are reused from pre_built//wheels. +# +# Usage: +# pre_built/build_scripts/build-cicwave.sh --tag 0.5.2 +# pre_built/build_scripts/build-cicwave.sh --tag 0.5.2 --source /path/to/checkout + +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLATFORM="el8.x86_64.glibc2p28" +WHEELS_DIR="$REPO/pre_built/$PLATFORM/wheels" +PATCH="$REPO/pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch" +CLONE_URL="https://github.com/wulffern/cicwave.git" +PYVER="3.14" +PLAT_TAG="manylinux_2_28_x86_64" +CHUNK_BYTES=$((40 * 1024 * 1024)) # 40 MiB -> stays under GitHub's 50 MB warn + +# cicwave's own PyQt6 closure NOT already bundled as shared wheels. numpy, +# pandas, click, pyyaml, packaging, python_dateutil, six are reused from the +# existing bundle (added by other python-tools) and intentionally NOT re-fetched. +DEP_SPECS=( + "PyQt6==6.9.1" "PyQt6-Qt6==6.9.2" "PyQt6-sip" pyqtgraph PyOpenGL + matplotlib contourpy cycler fonttools kiwisolver pillow pyparsing colorama +) + +tag="" +source_dir="" +while [ "$#" -gt 0 ]; do + case "$1" in + --tag) shift; [ "$#" -gt 0 ] || { echo "missing value for --tag" >&2; exit 2; }; tag="$1" ;; + --source) shift; [ "$#" -gt 0 ] || { echo "missing value for --source" >&2; exit 2; }; source_dir="$1" ;; + -h|--help) sed -n '2,/^$/p' "$0"; exit 0 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac + shift +done +[ -n "$tag" ] || { echo "ERROR: --tag X.Y.Z is required (stable release tag)" >&2; exit 2; } + +need() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }; } +need git; need uv; need pip3.14; need split +[ -r "$PATCH" ] || { echo "ERROR: patch not found: $PATCH" >&2; exit 1; } + +workdir="$(mktemp -d "${TMPDIR:-/tmp}/cicwave-build.XXXXXX")" +trap 'rm -rf "$workdir"' EXIT + +if [ -n "$source_dir" ]; then + src="$(cd "$source_dir" && pwd)" +else + src="$workdir/src" + git clone --depth 1 --branch "$tag" "$CLONE_URL" "$src" + echo "Applying PyQt6 port patch..." + git -C "$src" apply "$PATCH" +fi + +echo "Building cicwave $tag wheel via uv..." +mkdir -p "$WHEELS_DIR" +# Drop any prior cicwave wheel so a version bump does not leave a stale one. +rm -f "$WHEELS_DIR"/cicwave-*.whl +( cd "$src" && uv build --wheel --out-dir "$workdir/dist" ) +cp "$workdir/dist"/cicwave-*.whl "$WHEELS_DIR"/ +echo " -> $(basename "$(ls "$WHEELS_DIR"/cicwave-*.whl)")" + +echo "Downloading PyQt6 dependency closure (EL8 / cp$PYVER wheels)..." +# sip is a cp314-specific (non-abi3) wheel published only for older manylinux; +# allow the broader platform set so pip finds it alongside the 2_28 Qt wheels. +PIP_REQUIRE_VIRTUALENV=0 pip3.14 download "${DEP_SPECS[@]}" \ + --platform "$PLAT_TAG" --platform manylinux2014_x86_64 --platform manylinux_2_5_x86_64 --platform any \ + --python-version "$PYVER" --only-binary :all: --no-deps -d "$workdir/deps" +cp "$workdir/deps"/*.whl "$WHEELS_DIR"/ + +echo "Chunking wheels over $((CHUNK_BYTES / 1024 / 1024)) MiB (installer rejoins .whl.part-NNN)..." +for w in "$WHEELS_DIR"/*.whl; do + sz=$(stat -c %s "$w") + if [ "$sz" -gt "$CHUNK_BYTES" ]; then + echo " splitting $(basename "$w") ($((sz / 1024 / 1024)) MiB)" + rm -f "$w".part-* + split -d -a 3 -b "$CHUNK_BYTES" "$w" "$w".part- + rm -f "$w" + fi +done + +echo +echo "Done. Next:" +echo " python3 -m json.tool pre_built/packages.json >/dev/null # if you edited the registry" +echo " ./loadout completion bash > envs/bash/global/completions/loadout.bash" +echo " ./loadout install cicwave --dest-dir /tmp/t --no-backup # offline end-to-end smoke" +echo " git add pre_built/$PLATFORM/wheels pre_built/build_scripts/cicwave pre_built/build_scripts/build-cicwave.sh" diff --git a/pre_built/build_scripts/build-surfer.sh b/pre_built/build_scripts/build-surfer.sh new file mode 100755 index 00000000..88583cf1 --- /dev/null +++ b/pre_built/build_scripts/build-surfer.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Build Surfer (waveform viewer) for el8.x86_64.glibc2p28. +# +# Surfer is a Rust egui/glow (OpenGL) GUI. We build the latest STABLE tag from +# source on EL8 (glibc 2.28) -- upstream prebuilts target newer glibc. The +# release profile has no -march/target-cpu=native, so the artifact is portable +# across farm CPUs. Output is a bin-package pair (like gvim): +# +# pre_built//bin/surfer.bz2 POSIX-sh wrapper (Mesa/GLVND env) +# pre_built//bin/surfer.bin.bz2 real stripped ELF, RPATH $ORIGIN/../lib64 +# +# Runtime libs come from gui_libs (X11/Wayland/xkbcommon) + mesa3d_libs (Mesa +# vendor EGL/GBM/DRI). GLVND dispatchers (libGL.so.1) stay host-provided. +# +# Usage: +# pre_built/build_scripts/build-surfer.sh --tag v0.7.0 +# pre_built/build_scripts/build-surfer.sh --tag v0.7.0 --source /path/to/checkout +# +# After running: ./strip_all_elf_binaries && \ +# ./loadout completion bash > envs/bash/global/completions/loadout.bash + +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLATFORM="el8.x86_64.glibc2p28" +BIN_DIR="$REPO/pre_built/$PLATFORM/bin" +CLONE_URL="https://gitlab.com/surfer-project/surfer.git" +GCC_ENABLE="/opt/rh/gcc-toolset-14/enable" + +tag="" +source_dir="" +while [ "$#" -gt 0 ]; do + case "$1" in + --tag) + shift; [ "$#" -gt 0 ] || { echo "missing value for --tag" >&2; exit 2; } + tag="$1" ;; + --source) + shift; [ "$#" -gt 0 ] || { echo "missing value for --source" >&2; exit 2; } + source_dir="$1" ;; + -h|--help) + sed -n '2,/^$/p' "$0"; exit 0 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac + shift +done + +# Stable-release policy: a tag is mandatory and must look like a release tag. +[ -n "$tag" ] || { echo "ERROR: --tag vX.Y.Z is required (stable release tag)" >&2; exit 2; } +case "$tag" in + v[0-9]*) : ;; + *) echo "ERROR: --tag must be a stable release tag like v0.7.0 (got '$tag')" >&2; exit 2 ;; +esac + +need() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }; } +need git +need cargo +need patchelf +need strip +need bzip2 + +# Modern C toolchain: f128's __float128 shim wants quadmath; glibc floor stays +# at the system 2.28 regardless of gcc version. +[ -r "$GCC_ENABLE" ] && . "$GCC_ENABLE" + +workdir="$(mktemp -d "${TMPDIR:-/tmp}/surfer-build.XXXXXX")" +trap 'rm -rf "$workdir"' EXIT + +if [ -n "$source_dir" ]; then + src="$(cd "$source_dir" && pwd)" +else + src="$workdir/src" + # v0.7.0 vendors f128 + instruction-decoder as git submodules (path deps); + # they must be checked out or the workspace build fails to resolve f128. + git clone --depth 1 --branch "$tag" --recurse-submodules --shallow-submodules \ + "$CLONE_URL" "$src" +fi + +echo "Building surfer $tag from $src" + +# Fresh CARGO_HOME: the loadout's own ~/.cargo/config.toml redirects crates-io +# to the offline registry-store, which only holds the curated crate subset. +# Surfer's pinned deps need real crates.io, so isolate the registry/config here. +export CARGO_HOME="$workdir/cargo-home" +mkdir -p "$CARGO_HOME" + +( cd "$src" && cargo build --release -p surfer ) + +real="$src/target/release/surfer" +[ -x "$real" ] || { echo "ERROR: build produced no $real" >&2; exit 1; } + +# Portability gate: must run on EL8 glibc 2.28. +floor="$(readelf -V "$real" 2>/dev/null | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -V | tail -1)" +echo "glibc symbol floor: ${floor:-none}" +case "$floor" in + GLIBC_2.2[0-9]|GLIBC_2.1*|GLIBC_2.[0-9]|"") : ;; + *) echo "ERROR: $real needs $floor > GLIBC_2.28; not EL8-portable" >&2; exit 1 ;; +esac + +stage="$workdir/stage" +mkdir -p "$stage" +cp "$real" "$stage/surfer.bin" +strip "$stage/surfer.bin" +# Mesa vendor libs live one level up from bin/ at /lib64. +patchelf --set-rpath '$ORIGIN/../lib64' "$stage/surfer.bin" + +# Wrapper (single source of truth; mirrors wezterm's Mesa/GLVND env block). +cat > "$stage/surfer" <<'WRAP' +#!/bin/sh +# +# Surfer launcher (loadout). egui/glow (OpenGL) GUI. On headless farm nodes the +# GL stack is served by bundled Mesa userspace (mesa3d_libs) + GUI client libs +# (gui_libs); the real ELF is the sibling surfer.bin. GLVND dispatchers +# (libGL.so.1 ...) stay host-provided -- only the Mesa vendor side is bundled. + +bin_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P) || exit 1 +prefix=$(CDPATH= cd "$bin_dir/.." && pwd -P) || exit 1 + +mesa_libdir="$prefix/lib64" +if [ -d "$mesa_libdir" ]; then + export LD_LIBRARY_PATH="$mesa_libdir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + if [ -d "$mesa_libdir/dri" ]; then + export LIBGL_DRIVERS_PATH="$mesa_libdir/dri${LIBGL_DRIVERS_PATH:+:$LIBGL_DRIVERS_PATH}" + fi +fi +if [ -d "$prefix/share/glvnd/egl_vendor.d" ]; then + export __EGL_VENDOR_LIBRARY_DIRS="$prefix/share/glvnd/egl_vendor.d${__EGL_VENDOR_LIBRARY_DIRS:+:$__EGL_VENDOR_LIBRARY_DIRS}" +fi + +exec "$bin_dir/surfer.bin" "$@" +WRAP +chmod +x "$stage/surfer" + +mkdir -p "$BIN_DIR" +bzip2 -kf "$stage/surfer.bin" +bzip2 -kf "$stage/surfer" +cp "$stage/surfer.bin.bz2" "$BIN_DIR/surfer.bin.bz2" +cp "$stage/surfer.bz2" "$BIN_DIR/surfer.bz2" +chmod 644 "$BIN_DIR/surfer.bin.bz2" "$BIN_DIR/surfer.bz2" + +echo "Staged:" +echo " $BIN_DIR/surfer.bz2 (wrapper)" +echo " $BIN_DIR/surfer.bin.bz2 (ELF $floor)" +echo +echo "Next:" +echo " ./strip_all_elf_binaries" +echo " ./loadout completion bash > envs/bash/global/completions/loadout.bash" +echo " git add pre_built/$PLATFORM/bin/surfer.bz2 pre_built/$PLATFORM/bin/surfer.bin.bz2 .strip-manifest" diff --git a/pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch b/pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch new file mode 100644 index 00000000..f10ffde5 --- /dev/null +++ b/pre_built/build_scripts/cicwave/0001-port-pyside6-to-pyqt6.patch @@ -0,0 +1,417 @@ +diff --git a/pyproject.toml b/pyproject.toml +index 3d06466..101aad0 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -14,7 +14,7 @@ dependencies = [ + "matplotlib", # Engineering formatters (minimal dependency) + "click", # CLI parsing + "pyyaml", # Session files, pivot specs +- "PySide6", # Qt6 GUI framework ++ "PyQt6", # Qt6 GUI framework (PyQt6: EL8 glibc-2.28 + Python 3.14 wheels; PySide6 needs glibc 2.34) + "pyqtgraph", # High-performance plotting + "PyOpenGL", # GPU acceleration + ] +diff --git a/src/cicwave/wave_pg.py b/src/cicwave/wave_pg.py +index 250ee8b..326e9cb 100644 +--- a/src/cicwave/wave_pg.py ++++ b/src/cicwave/wave_pg.py +@@ -13,15 +13,15 @@ import numpy as np + import pandas as pd + from importlib.metadata import version as _pkg_version + +-from PySide6.QtWidgets import ( ++from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QSplitter, QTreeWidget, QTreeWidgetItem, QLineEdit, QTabWidget, + QPushButton, QLabel, QCheckBox, QTextEdit, QFileDialog, QDialog, + QInputDialog, QMenu, QComboBox, QTableWidget, QTableWidgetItem, + QHeaderView, QAbstractItemView, QMessageBox) +-from PySide6 import QtCore +-from PySide6.QtCore import Qt, Signal, QEvent, QSettings, QTimer +-from PySide6.QtGui import (QKeySequence, QFont, QFontDatabase, QShortcut, ++from PyQt6 import QtCore ++from PyQt6.QtCore import Qt, pyqtSignal as Signal, QEvent, QSettings, QTimer ++from PyQt6.QtGui import (QKeySequence, QFont, QFontDatabase, QShortcut, + QPainter, QColor, QPalette) + + import pyqtgraph as pg +@@ -33,7 +33,7 @@ from matplotlib.ticker import EngFormatter + + def _mono_font(size): + """Return the system fixed-width font at the given point size.""" +- f = QFontDatabase.systemFont(QFontDatabase.FixedFont) ++ f = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + f.setPointSize(size) + return f + +@@ -117,7 +117,7 @@ class _RotatedAxisItem(pg.AxisItem): + p.drawText( + QtCore.QRectF(-rect.height() / 2, -rect.width() / 2, + rect.height(), rect.width()), +- Qt.AlignCenter, text) ++ Qt.AlignmentFlag.AlignCenter, text) + p.restore() + + +@@ -165,9 +165,9 @@ class _SignalPickerDialog(QDialog): + self._table.setHorizontalHeaderLabels(["Signal", "File"]) + hdr = self._table.horizontalHeader() + hdr.setStretchLastSection(True) +- hdr.setSectionResizeMode(0, QHeaderView.Stretch) +- self._table.setSelectionBehavior(QAbstractItemView.SelectRows) +- self._table.setSelectionMode(QAbstractItemView.SingleSelection) ++ hdr.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) ++ self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) ++ self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self._table.setAlternatingRowColors(True) + for i, n in enumerate(self._names): + self._table.setItem(i, 0, QTableWidgetItem(n)) +@@ -659,8 +659,8 @@ class PgWaveBrowser(QWidget): + #- Allow shift/ctrl/cmd-click multi-select so the user can close + #- many files at once via the context menu. + self.file_tree.setSelectionMode( +- QAbstractItemView.ExtendedSelection) +- self.file_tree.setContextMenuPolicy(Qt.CustomContextMenu) ++ QAbstractItemView.SelectionMode.ExtendedSelection) ++ self.file_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.file_tree.customContextMenuRequested.connect( + self._file_context_menu) + +@@ -701,7 +701,7 @@ class PgWaveBrowser(QWidget): + self.wave_tree = _DropUrlTree() + self.wave_tree.setHeaderLabel("Waves") + self.wave_tree.itemDoubleClicked.connect(self._wave_clicked) +- self.wave_tree.setContextMenuPolicy(Qt.CustomContextMenu) ++ self.wave_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.wave_tree.customContextMenuRequested.connect(self._wave_context) + + layout.addWidget(self.file_tree, 1) +@@ -718,7 +718,7 @@ class PgWaveBrowser(QWidget): + f = self.files.open(fname, self.xaxis, sheet_name=sheet) + key = self.files.current + item = QTreeWidgetItem([f.name]) +- item.setData(0, Qt.UserRole, key) ++ item.setData(0, Qt.ItemDataRole.UserRole, key) + self.file_tree.addTopLevelItem(item) + self.file_tree.setCurrentItem(item) + self._fill_waves() +@@ -732,7 +732,7 @@ class PgWaveBrowser(QWidget): + f._original_path = original_path + key = self.files.current + item = QTreeWidgetItem([f.name]) +- item.setData(0, Qt.UserRole, key) ++ item.setData(0, Qt.ItemDataRole.UserRole, key) + self.file_tree.addTopLevelItem(item) + self.file_tree.setCurrentItem(item) + self._fill_waves() +@@ -741,19 +741,19 @@ class PgWaveBrowser(QWidget): + """Color a wave tree item to match its plot line.""" + item = self._tag_to_item.get(tag) + if item: +- from PySide6.QtGui import QColor ++ from PyQt6.QtGui import QColor + item.setForeground(0, QColor(color)) + + def clearWaveColor(self, tag): + """Reset a wave tree item color back to default.""" + item = self._tag_to_item.get(tag) + if item: +- from PySide6.QtGui import QColor ++ from PyQt6.QtGui import QColor + item.setForeground(0, QColor(_get_theme()['tree_default_fg'])) + + def _file_selected(self, current, previous): + if current: +- fname = current.data(0, Qt.UserRole) ++ fname = current.data(0, Qt.ItemDataRole.UserRole) + self.files.select(fname) + self._fill_waves() + +@@ -771,8 +771,8 @@ class PgWaveBrowser(QWidget): + item.setSelected(True) + selected = [item] + +- keys = [it.data(0, Qt.UserRole) for it in selected +- if it.data(0, Qt.UserRole)] ++ keys = [it.data(0, Qt.ItemDataRole.UserRole) for it in selected ++ if it.data(0, Qt.ItemDataRole.UserRole)] + if not keys: + return + +@@ -794,7 +794,7 @@ class PgWaveBrowser(QWidget): + self.files.remove(key) + for i in range(self.file_tree.topLevelItemCount()): + it = self.file_tree.topLevelItem(i) +- if it.data(0, Qt.UserRole) == key: ++ if it.data(0, Qt.ItemDataRole.UserRole) == key: + self.file_tree.takeTopLevelItem(i) + break + self._sync_file_selection_after_remove() +@@ -807,7 +807,7 @@ class PgWaveBrowser(QWidget): + return + for i in range(self.file_tree.topLevelItemCount()): + it = self.file_tree.topLevelItem(i) +- if it.data(0, Qt.UserRole) == cur: ++ if it.data(0, Qt.ItemDataRole.UserRole) == cur: + self.file_tree.setCurrentItem(it) + break + self._fill_waves() +@@ -868,7 +868,7 @@ class PgWaveBrowser(QWidget): + if self._flat_mode: + for name in sorted(names): + item = QTreeWidgetItem([name]) +- item.setData(0, Qt.UserRole, name) ++ item.setData(0, Qt.ItemDataRole.UserRole, name) + self.wave_tree.addTopLevelItem(item) + self._color_if_plotted(item, f, name) + return +@@ -898,13 +898,13 @@ class PgWaveBrowser(QWidget): + value = node[key] + if isinstance(value, str): + item = QTreeWidgetItem([key]) +- item.setData(0, Qt.UserRole, value) ++ item.setData(0, Qt.ItemDataRole.UserRole, value) + self._attach(parent, item) + self._color_if_plotted(item, wfile, value) + else: + item = QTreeWidgetItem([key]) + if None in value: +- item.setData(0, Qt.UserRole, value[None]) ++ item.setData(0, Qt.ItemDataRole.UserRole, value[None]) + self._color_if_plotted(item, wfile, value[None]) + self._attach(parent, item) + self._build_tree(item, value, wfile) +@@ -922,11 +922,11 @@ class PgWaveBrowser(QWidget): + if tag in self._wave_cache: + wave = self._wave_cache[tag] + if wave.color: +- from PySide6.QtGui import QColor ++ from PyQt6.QtGui import QColor + item.setForeground(0, QColor(wave.color)) + + def _wave_clicked(self, item, column): +- yname = item.data(0, Qt.UserRole) ++ yname = item.data(0, Qt.ItemDataRole.UserRole) + if not yname: + item.setExpanded(not item.isExpanded()) + return +@@ -947,7 +947,7 @@ class PgWaveBrowser(QWidget): + item = self.wave_tree.currentItem() + if item is None: + return None +- yname = item.data(0, Qt.UserRole) ++ yname = item.data(0, Qt.ItemDataRole.UserRole) + if not yname: + return None + f = self.files.getSelected() +@@ -988,7 +988,7 @@ class PgWaveBrowser(QWidget): + menu.exec(self.wave_tree.viewport().mapToGlobal(pos)) + return + +- yname = item.data(0, Qt.UserRole) ++ yname = item.data(0, Qt.ItemDataRole.UserRole) + if not yname: + menu = QMenu(self) + menu.addAction("Plot all visible waves", _plot_all_visible) +@@ -1269,7 +1269,7 @@ class PgWavePlot(QWidget): + #- and freeze the UI. We render at most ``_DIGITAL_MAX_LABELS`` + #- labels per signal, only for transitions inside the current + #- view rect, and refresh on demand. +- from PySide6.QtCore import QTimer as _QT ++ from PyQt6.QtCore import QTimer as _QT + self._digital_label_timer = _QT() + self._digital_label_timer.setSingleShot(True) + self._digital_label_timer.setInterval(60) +@@ -1451,7 +1451,7 @@ class PgWavePlot(QWidget): + base = pg.InfiniteLine( + pos=mid, angle=0, + pen=pg.mkPen(mono, width=0.5, +- style=Qt.DotLine)) ++ style=Qt.PenStyle.DotLine)) + self.digital_plot.addItem(base) + items['lines'].append(base) + else: +@@ -2340,7 +2340,7 @@ class PgWavePlot(QWidget): + def _make_cursor_line(self, x, color, which): + line = pg.InfiniteLine( + pos=x, angle=90, movable=True, +- pen=pg.mkPen(color, width=1, style=Qt.DashLine)) ++ pen=pg.mkPen(color, width=1, style=Qt.PenStyle.DashLine)) + line.sigPositionChanged.connect( + lambda l, w=which: self._cursor_dragged(w, l)) + return line +@@ -2400,7 +2400,7 @@ class PgWavePlot(QWidget): + self._wheel_pending_x_scale = 1.0 + self._wheel_pending_y_scale = 1.0 + +- if modifiers & Qt.ShiftModifier: ++ if modifiers & Qt.KeyboardModifier.ShiftModifier: + self._wheel_pending_y_scale *= scale + else: + self._wheel_pending_x_scale *= scale +@@ -2438,7 +2438,7 @@ class PgWavePlot(QWidget): + + def _on_mouse_drag(self, ev, axis=None): + mods = ev.modifiers() +- if ev.button() == Qt.RightButton: ++ if ev.button() == Qt.MouseButton.RightButton: + #- Right-drag always draws a rubber band. Modifiers constrain + #- the resulting zoom to a single axis: + #- plain -> rectangle, zoom both x and y +@@ -2446,8 +2446,8 @@ class PgWavePlot(QWidget): + #- Ctrl (or Cmd on Mac) -> band spans full plot width, zoom y only + #- On macOS Qt swaps the physical Ctrl and Meta keys by + #- default, so accept either modifier to mean "y-only". +- ctrl_like = (Qt.ControlModifier | Qt.MetaModifier) +- if mods & Qt.ShiftModifier: ++ ctrl_like = (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.MetaModifier) ++ if mods & Qt.KeyboardModifier.ShiftModifier: + self._right_drag_rubber_band(ev, constrain='x') + elif mods & ctrl_like: + self._right_drag_rubber_band(ev, constrain='y') +@@ -2468,10 +2468,10 @@ class PgWavePlot(QWidget): + """ + ev.accept() + vb = self.plot.vb +- from PySide6.QtCore import QRectF, QPointF ++ from PyQt6.QtCore import QRectF, QPointF + from pyqtgraph import Point + +- down_pos = ev.buttonDownPos(Qt.RightButton) ++ down_pos = ev.buttonDownPos(Qt.MouseButton.RightButton) + cur_pos = ev.pos() + + #- Build the on-screen (parent-coords) rectangle with the chosen +@@ -2878,7 +2878,7 @@ class PgAnalysisPlot(QWidget): + + def _set_cursor(self, which, x): + pen = pg.mkPen('y' if which == 'a' else 'g', +- width=1, style=Qt.DashLine) ++ width=1, style=Qt.PenStyle.DashLine) + attr_line = '_cursor_%s_line' % which + old = getattr(self, attr_line) + if old: +@@ -2904,7 +2904,7 @@ class PgAnalysisPlot(QWidget): + pos = event.pos() + mouse_point = vb.mapSceneToView(pos) + modifiers = event.modifiers() +- if modifiers & Qt.ShiftModifier: ++ if modifiers & Qt.KeyboardModifier.ShiftModifier: + vr = vb.viewRange() + ylo, yhi = vr[1] + yd = mouse_point.y() +@@ -2920,8 +2920,8 @@ class PgAnalysisPlot(QWidget): + + def _on_mouse_drag(self, ev, axis=None): + mods = ev.modifiers() +- if ev.button() == Qt.RightButton and ( +- mods & Qt.ShiftModifier or mods & Qt.ControlModifier): ++ if ev.button() == Qt.MouseButton.RightButton and ( ++ mods & Qt.KeyboardModifier.ShiftModifier or mods & Qt.KeyboardModifier.ControlModifier): + ev.accept() + if ev.isFinish(): + return +@@ -2930,12 +2930,12 @@ class PgAnalysisPlot(QWidget): + vr = vb.viewRange() + w = vb.width() + h = vb.height() +- if mods & Qt.ShiftModifier: ++ if mods & Qt.KeyboardModifier.ShiftModifier: + dx = delta.x() / w + xlo, xhi = vr[0] + xspan = xhi - xlo + vb.setXRange(xlo + dx * xspan, xhi - dx * xspan, padding=0) +- elif mods & Qt.ControlModifier: ++ elif mods & Qt.KeyboardModifier.ControlModifier: + dy = delta.y() / h + ylo, yhi = vr[1] + yspan = yhi - ylo +@@ -2999,7 +2999,7 @@ class PgWaveWindow(QMainWindow): + self.setWindowTitle("cIcWave v%s: %s" % (ver, os.getcwd())) + self.resize(1200, 700) + +- splitter = QSplitter(Qt.Horizontal) ++ splitter = QSplitter(Qt.Orientation.Horizontal) + self.setCentralWidget(splitter) + + self.browser = PgWaveBrowser(xaxis) +@@ -3049,14 +3049,14 @@ class PgWaveWindow(QMainWindow): + + def eventFilter(self, obj, event): + et = event.type() +- if et in (QEvent.DragEnter, QEvent.DragMove, QEvent.Drop): ++ if et in (QEvent.Type.DragEnter, QEvent.Type.DragMove, QEvent.Type.Drop): + if obj in self._drops_delegate_trees: + return False +- if et == QEvent.DragEnter or et == QEvent.DragMove: ++ if et == QEvent.Type.DragEnter or et == QEvent.Type.DragMove: + if self._drop_mime_has_files(event.mimeData()): + event.acceptProposedAction() + return True +- elif et == QEvent.Drop: ++ elif et == QEvent.Type.Drop: + self._handle_file_drop(event) + return True + return super().eventFilter(obj, event) +@@ -3157,10 +3157,10 @@ class PgWaveWindow(QMainWindow): + #- Single-letter shortcuts that should fire even while the + #- wave/file tree has focus. ``ApplicationShortcut`` context + #- bypasses the QTreeWidget's keyboard-search swallowing. +- from PySide6.QtCore import Qt as _Qt ++ from PyQt6.QtCore import Qt as _Qt + d_sc = QShortcut(QKeySequence("D"), self, + self.browser.togglePlotAsDigital) +- d_sc.setContext(_Qt.ApplicationShortcut) ++ d_sc.setContext(_Qt.ShortcutContext.ApplicationShortcut) + + def keyPressEvent(self, event): + if isinstance(self.focusWidget(), QLineEdit): +@@ -3169,7 +3169,7 @@ class PgWaveWindow(QMainWindow): + p = self._current() + key = event.text() + mods = event.modifiers() +- ctrl_like = (Qt.ControlModifier | Qt.MetaModifier) ++ ctrl_like = (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.MetaModifier) + if p and key == 'z' and not (mods & ctrl_like) and hasattr(p, 'zoomIn'): + p.zoomIn() + elif p and key == 'Z' and hasattr(p, 'zoomOut'): +@@ -3482,7 +3482,7 @@ class PgWaveWindow(QMainWindow): + "(time, frequency, sweeps, …).\n" + "Leave empty for automatic detection.\n" + "Applies to newly opened files; saved for later sessions.", +- QLineEdit.Normal, ++ QLineEdit.EchoMode.Normal, + self.browser.xaxis or "") + if not ok: + return +@@ -3518,7 +3518,7 @@ class PgWaveWindow(QMainWindow): + btn.clicked.connect(dlg.accept) + form.addWidget(btn) + +- if dlg.exec() != QDialog.Accepted: ++ if dlg.exec() != QDialog.DialogCode.Accepted: + return + p.custom_title = title_le.text().strip() or None + p.custom_xlabel = xlabel_le.text().strip() or None +@@ -3839,7 +3839,7 @@ class PgWaveWindow(QMainWindow): + names = list(f.getWaveNames()) + dlg = _SignalPickerDialog(self, "Select X-axis signal", names, + file_label=f.name) +- if dlg.exec() != QDialog.Accepted: ++ if dlg.exec() != QDialog.DialogCode.Accepted: + return + chosen = dlg.selected() + if not chosen: +@@ -3875,9 +3875,9 @@ def _apply_theme(app, theme_name='dark'): + app.setStyle("Fusion") + p = QPalette() + for role_name, rgb in theme['palette'].items(): +- p.setColor(getattr(QPalette, role_name), QColor(*rgb)) +- p.setColor(QPalette.Disabled, QPalette.Text, QColor(128, 128, 128)) +- p.setColor(QPalette.Disabled, QPalette.ButtonText, QColor(128, 128, 128)) ++ p.setColor(getattr(QPalette.ColorRole, role_name), QColor(*rgb)) ++ p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(128, 128, 128)) ++ p.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(128, 128, 128)) + app.setPalette(p) + pg.setConfigOptions(background=theme['pg_background'], + foreground=theme['pg_foreground']) diff --git a/pre_built/build_scripts/farm-versions b/pre_built/build_scripts/farm-versions index f16b7fce..24746973 100755 --- a/pre_built/build_scripts/farm-versions +++ b/pre_built/build_scripts/farm-versions @@ -459,6 +459,13 @@ TOOLS = [ "https://github.com/eteran/nedit-ng", strategy_flag(["--version"], r"([0-9]+\.[0-9]+\.[0-9]+)"), ), + ( + "surfer", + "surfer", + "https://gitlab.com/surfer-project/surfer", + # surfer --version -> "surfer 0.7.0 (git: v0.7.0)" + strategy_flag(["--version"], r"surfer ([0-9]+\.[0-9]+\.[0-9]+)"), + ), ( "xterm", "xterm", diff --git a/pre_built/el8.x86_64.glibc2p28/bin/surfer.bin.bz2 b/pre_built/el8.x86_64.glibc2p28/bin/surfer.bin.bz2 new file mode 100644 index 00000000..6c2dee14 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/bin/surfer.bin.bz2 differ diff --git a/pre_built/el8.x86_64.glibc2p28/bin/surfer.bz2 b/pre_built/el8.x86_64.glibc2p28/bin/surfer.bz2 new file mode 100644 index 00000000..a303a966 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/bin/surfer.bz2 differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/cicwave-0.5.2-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/cicwave-0.5.2-py3-none-any.whl new file mode 100644 index 00000000..030afb91 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/cicwave-0.5.2-py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/colorama-0.4.6-py2.py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/colorama-0.4.6-py2.py3-none-any.whl new file mode 100644 index 00000000..f666ce98 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/colorama-0.4.6-py2.py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..16e89a03 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/cycler-0.12.1-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/cycler-0.12.1-py3-none-any.whl new file mode 100644 index 00000000..6478c3f0 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/cycler-0.12.1-py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 00000000..54fcfdc0 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 00000000..ee00cbde Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..15b12de1 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..ef16cc8d Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyopengl-3.1.10-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pyopengl-3.1.10-py3-none-any.whl new file mode 100644 index 00000000..8e17c47c Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyopengl-3.1.10-py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyparsing-3.3.2-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pyparsing-3.3.2-py3-none-any.whl new file mode 100644 index 00000000..ae2d24c0 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyparsing-3.3.2-py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..58d435d0 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-000 b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-000 new file mode 100644 index 00000000..ce4ea046 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-000 differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-001 b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-001 new file mode 100644 index 00000000..32234ef0 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_qt6-6.9.2-py3-none-manylinux_2_28_x86_64.whl.part-001 differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_sip-13.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_sip-13.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl new file mode 100644 index 00000000..64da9ec2 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyqt6_sip-13.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/pyqtgraph-0.14.0-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/pyqtgraph-0.14.0-py3-none-any.whl new file mode 100644 index 00000000..912f6770 Binary files /dev/null and b/pre_built/el8.x86_64.glibc2p28/wheels/pyqtgraph-0.14.0-py3-none-any.whl differ diff --git a/pre_built/el8.x86_64.glibc2p28/wheels/time_plot-0.1.0-py3-none-any.whl b/pre_built/el8.x86_64.glibc2p28/wheels/time_plot-0.1.0-py3-none-any.whl index b5185436..7eeba0a2 100644 Binary files a/pre_built/el8.x86_64.glibc2p28/wheels/time_plot-0.1.0-py3-none-any.whl and b/pre_built/el8.x86_64.glibc2p28/wheels/time_plot-0.1.0-py3-none-any.whl differ diff --git a/pre_built/packages.json b/pre_built/packages.json index 79434024..c8f9c653 100644 --- a/pre_built/packages.json +++ b/pre_built/packages.json @@ -1380,6 +1380,29 @@ ], "description": "Qt5 GUI text editor, NEdit rewrite" }, + "surfer": { + "kind": "bin", + "bins": [ + "surfer", + "surfer.bin" + ], + "version": "0.7.0", + "platforms": [ + "linux" + ], + "tags": [ + "gui", + "opengl", + "waveform", + "eda" + ], + "optional": true, + "depends": [ + "gui_libs", + "mesa3d_libs" + ], + "description": "Surfer — waveform viewer (VCD/FST/GHW) for digital hardware debugging; egui/OpenGL GUI. Opt-in: install with ./loadout install surfer" + }, "nvim-qt": { "kind": "bin", "bins": [ @@ -2154,7 +2177,7 @@ ], "uv_tool": "time-plot", "rolling_git": "https://github.com/smprather/time-plot.git", - "version": "2a1c077", + "version": "4e94f23", "platforms": [ "linux" ], @@ -2205,6 +2228,50 @@ ], "description": "text-serdes \u2014 short-lived encrypted text transport for copy/paste workflows (enc/dec)" }, + "cicwave": { + "kind": "python-tool", + "bins": [], + "wheels": [ + "cicwave", + "PyQt6", + "PyQt6_Qt6", + "PyQt6_sip", + "pyqtgraph", + "PyOpenGL", + "matplotlib", + "contourpy", + "cycler", + "fonttools", + "kiwisolver", + "pillow", + "pyparsing", + "colorama", + "numpy", + "pandas", + "click", + "pyyaml", + "packaging", + "python_dateutil", + "six" + ], + "uv_tool": "cicwave", + "version": "0.5.2", + "platforms": [ + "linux" + ], + "tags": [ + "gui", + "waveform", + "eda", + "python" + ], + "optional": true, + "depends": [ + "portable-python", + "uv" + ], + "description": "cicwave — PyQtGraph waveform viewer (ngspice/Xyce/VCD/CSV). loadout PyQt6 fork of the upstream PySide6 app (PySide6 has no EL8 glibc-2.28 + Python 3.14 wheel). Opt-in: ./loadout install cicwave" + }, "pygwalker": { "kind": "python-tool", "bins": [],