Chore/rebrand squatchlab 78#3
Open
squatchlab wants to merge 33 commits into
Open
Conversation
Closes the QSettings collision the audit flagged. The bare brand
'HDZero/Programmer' was shared with the macOS upstream — a host running
both forks would see one tool's autobackup toggle clobber the other's.
New scope: lab.squatch / hdzero-programmer-linux. File now lands at
~/.config/lab.squatch/hdzero-programmer-linux.conf.
Migration: app_settings.migrate_settings_once() runs in main() after
_install_excepthook(). On first launch under the new scope it copies all
keys from the legacy QSettings('HDZero', 'Programmer') store into the
new one, then writes a sentinel '_migrated_from_legacy_org' so reruns
short-circuit. Existing keys in the new scope are NOT overwritten — if a
user somehow set the new scope before migration ran, their explicit
choice wins. Legacy file is left intact so a downgrade still finds its
data.
New module app_settings.py centralizes the org/app constants so
main.py and internet_panel.py stop duplicating them. Added to
[tool.setuptools].py-modules and the AppImage inject list. CLAUDE.md
module list bumps from five to six modules and the autobackup gotcha
points at the new helper.
Tests: 5 new in tests/test_app_settings.py covering scope path, legacy
copy, idempotency via sentinel, no-overwrite of new-scope values, and
no-legacy-data noop.
Closes #33.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PyQt6 6.8 wheels dlopen libGL.so.1 from QtGui at import time, even when
QT_QPA_PLATFORM=offscreen. Earlier CI runs all sat queued because no
Forgejo runner was registered, so this never surfaced. With the runner
now online (lab01-hdzero), the smoke job's pytest step fails on:
E ImportError: libGL.so.1: cannot open shared object file:
No such file or directory
caught at tests/test_excepthook.py and tests/test_version_consistency.py
collection time (both import main, which imports PyQt6.QtGui).
Adds libgl1 to the apt-get list. Comment updated to flag why this
package is required despite headless Qt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI surfaced a real bug now that the runner is online: FAILED tests/test_app_settings.py::test_migrate_copies_legacy_keys_once AssertionError: assert True is False Two distinct fixes, both in scope for the same problem: 1. `migrate_settings_once()` now calls `new.sync()` after writing the migrated keys + sentinel. QSettings auto-syncs lazily; a same-process reader constructing a fresh `QSettings(SETTINGS_ORG, SETTINGS_APP)` right after migration would otherwise miss the in-flight values until the timer fires (or the original instance gets GC'd). The first user launch under v0.3 hits this path exactly once, so getting it right matters even outside the test. 2. `tests/test_app_settings.py::isolated_config` now pins the QSettings user-scope IniFormat path via `QSettings.setPath()` instead of relying on `XDG_CONFIG_HOME` alone. PyQt6's QStandardPaths caches the resolved config dir per-process, so an env var monkeypatched mid-suite doesn't always reach a QSettings constructed later — which is why the legacy keys appeared empty to `migrate_settings_once()` under pytest. Reset on teardown so the override doesn't leak to a subsequent test. Refs #33 (the QSettings reverse-DNS scope feature this test is exercising). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tests PR #61's first attempt (XDG isolation + QSettings.setPath) didn't work: PyQt6's QStandardPaths cache + the autouse QCoreApplication fixture race meant the QSettings instances inside migrate_settings_once still read from a path the test couldn't predict. CI still showed: FAILED tests/test_app_settings.py::test_migrate_copies_legacy_keys_once AssertionError: assert True is False Refactor: 1. `migrate_settings_once()` now takes optional `new=` and `old=` QSettings kwargs. Production path is unchanged (defaults call `settings()` and construct the legacy scope), but tests can inject QSettings instances pinned to explicit on-disk paths via the `QSettings(filename, IniFormat)` overload — sidestepping QStandardPaths entirely. 2. Test suite rewritten to use a `settings_pair` fixture that returns two empty IniFormat QSettings backed by tmp_path files. Hermetic; no env var monkeypatching required. Added one extra test that confirms `settings()` returns a handle with the right organization + application names (so the reverse-DNS pin is enforced from both directions). Refs #33. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
silent child `for line in proc.stdout:` blocks on readline until EOF. A wedged flashrom that prints nothing after starting (or a test like `echo started; sleep 30`) would burn through the entire wall-clock budget before the deadline check ever ran — which is why test_run_admin_streaming_terminates_on_timeout has been failing on the new Forgejo runner with `timeout path took too long: 30.0s` (real sleep duration). Replaces the readline loop with a `select.select(..., poll=1.0)` loop that wakes up at least once per second to re-check the deadline. Reads are now via `os.read` on the raw fd; line splitting happens in-process on a small buffer. EOF handling preserved (any unterminated tail is flushed via line_cb before falling through to proc.wait). Also factored the SIGTERM->SIGKILL escalation into a small inner helper so both the streaming-loop timeout path and the post-stdout-close timeout path share the exact same cleanup. Behaviour-equivalent for happy-path runs that print regularly; only changes the silent-child case from "hangs until EOF" to "kills on deadline." Production matters too: a flashrom that wedges mid-erase without writing further markers could otherwise hold the GUI for the default 600s timeout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI smoke + appimage jobs use actions/upload-artifact@v4. With the Forgejo runner online (lab01-hdzero), the action errors with: GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES. Forgejo emulates the GitHub Actions API enough to run most workflows, but the v4 line of upload-artifact requires GitHub's hosted artifact service that GHES (and Forgejo by extension) doesn't expose. The v3 line uses the older API and works. The bump from v3 -> v4 (commit f039ee5, "chore: NIT bundle + actions/upload-artifact v4") was speculative — CI was queued forever on a missing runner at that point so the regression never showed up. Now that CI executes, roll back to v3 across all three usages: the coverage-xml upload in smoke, and the AppImage uploads in both ci.yml and release.yml. If we ever migrate to a hosted-artifact-capable Forgejo (or move to forgejo/upload-artifact@v4 which is API-compatible with v4), this can be re-bumped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
python-appimage==0.34 imports distutils.dir_util in fs.py. distutils was removed from the standard library in Python 3.12, so the appimage job's `pipx install 'python-appimage==0.34'` succeeds but the build crashes immediately: ModuleNotFoundError: No module named 'distutils' Caught the moment the runner came online (CI was queued forever before, so the regression sat hidden). 1.4+ uses pathlib/shutil instead of distutils. Bound to <2 so a major-version SemVer break trips a deliberate revisit. Both ci.yml (PR + main pushes) and release.yml (v* tag pushes) bumped in lockstep so a tag-cut release doesn't fall off a different cliff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
python-appimage 1.4+ uses an externally-installed appimagetool which in turn needs `mksquashfs` from the squashfs-tools package. The bump to 1.x exposed: mksquashfs command is missing but required, please install it 0.34 may have shipped its own bundled tool; 1.x explicitly delegates. Both ci.yml (smoke + appimage) and release.yml (v* tag pushes) need the package since both run the same build script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same root cause as the smoke job (fixed in PR #60): PyQt6.QtGui dlopens libGL.so.1 even when the eventual exec is `--version` / `--check-rule` and never paints. The smoke launch step: out=$(./dist/HDZeroProgrammer-x86_64.AppImage --version) triggers the imports before argparse runs, so it errors with: ImportError: libGL.so.1: cannot open shared object file Both ci.yml's `appimage` job and release.yml's `release` job build + launch-smoke the AppImage; both need libgl1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous round added libgl1; surfaced the next missing one: ImportError: libEGL.so.1: cannot open shared object file PyQt6.QtGui dlopens an entire Qt platform stack at import. The smoke job already installed libgl1 + libegl1 + libxkbcommon0 + libdbus-1-3 + libfontconfig1 + libxcb-cursor0; the appimage and release jobs need the same set because the launch-smoke step (`--version` / `--check-rule`) imports PyQt6 before argparse can short-circuit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…5, #37) Closes the architecture+ops bundle from the audit: - #25: Four near-duplicate `*Worker(QThread)` classes (LoadDevices/LoadFirmwares/LoadImage/DownloadFirmware) in internet_panel.py shared identical shape but had drifted timeouts (15/15/10/30s, copy-pasted) and no shared retry policy. - #37: A single transient network blip forced the user to click Retry. Quality-of-life fix paired with the collapse. Replaces all four with one `HttpWorker(QThread)` taking a `parser` hook (JSON-shaped result), `stream_to_temp_bin` (firmware download), or neither (raw bytes — image fetch). Existing call sites updated to the same shape; per-call timeout is preserved (image keeps 10s, download keeps 30s, list endpoints take the new 15s default). Retry policy: `retries` default 2 + `backoff` default 1.0s gives a 1s, 2s exponential ramp before giving up. `time.sleep` runs on the worker thread so the GUI doesn't block. Each retry surfaces via a new `log` signal that InternetPanel forwards into `status_box`, so a user investigating a flake sees the attempts. internet_panel.py LOC: 491 → 423 (-68 lines, easily clears the ≥25 acceptance criterion). Tests: 7 new in tests/test_http_worker.py covering parser path, raw bytes, streamed download, retry-then-success, retry-exhausted, default retry count, default timeout. Same `worker.run()`-synchronous pattern the flash integration tests use; `requests.get` and `time.sleep` monkeypatched per-test. Closes #25, closes #37. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the BLOCKER from the audit. Every test up to now mocked flashrom via tests/fixtures/fake_flashrom.sh; a flashrom upgrade that broke the argv contract or the stdout phase markers FlashWorker greps for would ship a broken AppImage with green CI. New `.forgejo/workflows/hw-test.yml` runs the read -> pad -> write -> verify -> restore round-trip against real silicon. Gating: - `runs-on: [ubuntu-22.04, hdzero-hw]` so the job is naturally inert on the default `lab01-hdzero` runner (which advertises only `ubuntu-22.04`). Adding a hardware runner is opt-in. - `on: push: branches: [main]` + `workflow_dispatch` only. PR runs are deliberately excluded — running arbitrary contributor changes against real hardware is a bricking foot-gun. - `restore` step runs with `if: always()` so an aborted job still leaves the VTX bootable. Acceptance criterion: idempotent teardown. - All four flashrom transcripts (read/write/verify/restore) upload as a build artifact (`hw-test-transcripts`) for post-mortem. The workflow uses `make_padded_image_1mib` from flash_ops to produce the 1 MiB-padded blob, which means a regression in the padding logic would surface here too rather than only in unit tests. `docs/HARDWARE-CI.md` documents the runner setup: hardware wiring, udev rule install, Quadlet stanza with USB pass-through, the `/var/lib/hdzero-hw/known-good-firmware.bin` staging path, label matrix, and the rationale for not gating on PRs. References the existing CH341A udev rule and packaging/install-udev.sh so the hw-runner setup reuses what's already in the repo. Closes #22. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audit MINOR ACQUISITION ticket. The fork uses the bare brand "HDZero" across pyproject name, README, app title, AppImage filename, and Forgejo repo slug. Divimath Inc. owns the product trademark; this project is unaffiliated. An acquirer's diligence team will ask the question — better to find a documented "open with contingency" than a silent gap. `docs/legal/trademark.md`: - Catalogues every place the brand surfaces (so a future renaming pass has a checklist). - Lists the open research questions: USPTO TESS lookup, Divimath trademark policy, maintainer risk-tolerance call. - Names a concrete rename contingency (`vtx-programmer-linux`) with the file-by-file delta — including reuse of `migrate_settings_once` for the QSettings scope hop. - States the disposition: keep the name today, point diligence at this doc, execute the contingency on a cease-and-desist trigger. CONTRIBUTING.md gets a one-line pointer at the trademark file from the License section so contributors see the exposure before mailing patches with their preferred branding. Closes #43. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the QUALITY MAJOR from the audit (#28). Pyflakes catches undefined names; mypy catches signature mismatches, Optional handling, and the long tail of "None where Path was expected" bugs that ship as AttributeError at runtime — and for a tool that flashes hardware, type discipline on FlashWorker, the privilege-escalation argv builder, and `_open_flash_log` is cheap insurance. Config: - `[tool.mypy]` checks all six top-level modules. - `[[tool.mypy.overrides]]` for `flash_ops`/`udev_check`/`app_logging`/ `app_settings` runs strict mode (full signature typing, Optional-strict, no implicit Any). - `[[tool.mypy.overrides]]` for `main`/`internet_panel` enforces `disallow_untyped_defs` — every function signature is typed but UI glue avoids the Any ocean a full strict pass would produce. - PyQt6 namespace marked `ignore_missing_imports`. Community PyQt6-stubs lags the wheel cadence and breaks CI when out of sync; treating Qt symbols as Any is a deliberate trade-off documented in the config comments. Dev deps add `mypy>=1.10` + `types-requests`. CI runs `mypy` after ruff and before gitleaks; first stable run becomes the new floor. Also: `py_compile` step adds `app_settings.py` (missed in the original list when the module landed in PR #58). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First mypy run on the Forgejo runner surfaced 55 errors across main.py and internet_panel.py — every Qt callback would need a return-type annotation under `disallow_untyped_defs = true`. The strict block on the four safety-critical modules (flash_ops / udev_check / app_logging / app_settings) passed cleanly with zero errors. Loosens the UI override to `disallow_untyped_defs = false` + `check_untyped_defs = false`. The result: mypy still catches cross- module signature drift (e.g., a flash_ops.py change that breaks main.py callers — the original concrete value of typing UI code at all) but doesn't force an annotation sweep through every QWidget subclass as part of this PR. Tightening main + internet_panel to full strict typing is a deliberate follow-up — call out in `pyproject.toml [tool.mypy]` comments. The first stable mypy run becomes the new floor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rict misses Second mypy run on the runner surfaced the real shape of the gap. flash_ops.py was supposed to be strict-clean but had four genuine strict-mode misses: - `run_admin` returned `subprocess.CompletedProcess` without the `[str]` type argument (the function does pass `text=True`). - `FlashWorker.run`, `BackupWorker.run`, and the inner `on_line` callback all lacked `-> None`. QThread's `.run()` is overridden, so mypy can't infer the return type from a base class with `Any`-typed signatures (PyQt6 has no stubs). Added explicit `-> None` to all three. internet_panel.py errors were 13 missing-return-type warnings on Qt slots and helpers. The previous attempt (`disallow_untyped_defs = false` + `check_untyped_defs = false` override) did not suppress them — mypy 1.10's override resolution evidently still applies the `no-untyped-def` check despite both flags being false. Switched to `ignore_errors = true`. mypy still parses both modules so a real import-time break surfaces, but per-function annotation gaps stop failing CI. A follow-up sweep can re-enable the gate later. The strict block on flash_ops, udev_check, app_logging, app_settings is unchanged and (after this PR's fixes to flash_ops) passes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… (#68) Replaces the workaround `ignore_errors = true` from PR #65 with full `disallow_untyped_defs = true` + `check_untyped_defs = true`. Annotated 17 functions in main.py and 18 in internet_panel.py — most are Qt slots / callbacks that simply gain `-> None`. A few cases needed real type information: - `LocalPanel.__init__` callbacks now typed as `Callable[[], None]` and `Callable[[str, bool], None]`. - `_install_excepthook`'s inner `hook(exc_type, exc, tb)` typed per the `sys.excepthook` contract: `type[BaseException]`, `BaseException`, `Optional[TracebackType]`. - `main(argv=...)` typed as `Optional[list[str]]` matching argparse. - `on_devices_ok` / `on_fw_ok` parameterized their `list` arg as `list[dict[str, Any]]` (the API JSON shape). The strict block on the four safety-critical modules (flash_ops/udev_check/app_logging/app_settings) is unchanged; this just brings UI plumbing under the same kind of signature-checking discipline. Cross-module drift between flash_ops and main now surfaces from mypy rather than at runtime as an AttributeError. Closes #68. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI mypy round on PR #69 surfaced two real type gaps: - main.py:316 — `open_flash_log` returns `Tuple[Path, IO[str]]`, so mypy infers `_flash_log_path: Path` and `_flash_log_fh: IO[str]`. The OSError fallback assigning `None` then errors. The intent was always Optional — the close path uses `getattr(... None)` and the write path early-returns when `fh is None`. Declare both attributes upfront in `MainWindow.__init__` as `Optional[Path]` / `Optional[IO[str]]`. Adds `IO` to the typing import. - internet_panel.py:140-141 — `List[dict]` is bare-generic. The API shape is `list[dict[str, Any]]` (per the slot signatures already fixed in this branch). Parameterize for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…)' (#69) from ticket-68-mypy-tighten into main
…RITY) Sweep four top-level docs against the post-audit batch (8+ PRs, app_settings.py landed, HW-CI workflow shipped). Fixes: CLAUDE.md - open-blocker backlog #19-#23 -> #19-#21 (#22/#23 closed) - "five modules" py_compile list -> six (app_settings.py) - CI cascade missing mypy step now noted with override rationale - hw-test workflow added to CI/release section README.md - "Five Python modules" -> six, with app_settings.py bullet - "43 tests" -> 64 - CI section gains mypy + gitleaks + AppImage smoke flags CHANGELOG.md ([Unreleased]) - Add app_settings reverse-DNS migration (#33), HttpWorker collapse (#25/#37), mypy gate (#26/#68), hw-test workflow (#22), trademark.md (#43), python-appimage 1.x bump, full Qt runtime libs in CI, select.select() fix in run_admin_streaming - Move "upload-artifact bumped to v4" to a Reverted section -- Forgejo doesn't support the v4 upload protocol yet (rolled back to v3) SECURITY.md - In-scope module list missing app_settings.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…DME, CHANGELOG, SECURITY)' (#70) from docs/post-marathon-drift-refresh into main
Bump pyproject + main.__version__ to 0.3.0. Move CHANGELOG [Unreleased] body into [0.3.0] - 2026-04-30 block. SECURITY.md supported-versions table → 0.3.x. README reorder: lead with AppImage download (recommended), demote pip-install to from-source appendix; collapse local AppImage-build section to maintainer note. Closes the audit-batch documentation cycle. Firmware-trust trio (#19/#20/#21) remains gating set for v0.4.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ctrl-C on the terminal eventually unwinds out of app.exec(), and the excepthook installed by _install_excepthook then routed it through the crash-dialog path — popping "HDZero Programmer crashed" with a traceback ending at `return app.exec()`. Special-case KeyboardInterrupt at the top of the hook: call QApplication.quit() and return early, skipping the dialog and the stderr dump. Real crashes still get the dialog + transcript-dir hint. Closes #74 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "From source" and Development sections recommended `pip install --user`, which fails on modern distros under PEP 668 with `error: externally-managed-environment`. They also omitted system-level deps (xcb-util-cursor for Qt 6.5+) and the graphical-session prereq, leaving headless/TTY users stuck on `qt.qpa.xcb: could not connect to display`. Rewrite around `python3 -m venv .venv`, document the system deps in Requirements, add a one-line graphical-session note pointing at Moonlight/Sunshine/RDP/VNC for headless boxes, and drop the stale "64 tests" count from Development. AppImage path (Option A) unchanged. Closes #76 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#78) The window title carried "by Gunther_FPV" since the upstream macOS port. Fork has matured beyond drop-in adapter; rebrand the visible title to SquatchLab while keeping Gunther's attribution intact in every place MIT actually requires it (LICENSE, LICENSE-NOTES.md, README Credits) and in places users actually read it (Help tab renders README at runtime, so the Credits section remains visible inside the running app). Also append a small SquatchLab footer to the README so the Help tab inside the app renders a link to squatchlab.com — a more useful target for click-through than the title bar. Closes #78 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… (#74)' (#75) from fix/excepthook-keyboardinterrupt-74 into main
…v + system deps (#76)' (#77) from docs/readme-fromsource-venv-76 into main
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.