ScanSlicer/MAINTENANCE.md

267 lines
10 KiB
Markdown
Raw Permalink Normal View History

# Maintaining ScanSlicer
This file is a how-to for keeping the fork alive over time. The goals are
narrow and deliberate:
1. **Keep the app functional** for its one real job (detect photos in a flatbed
scan, correct the boxes, export straightened crops).
2. **Avoid security issues and breakage from end-of-life (EOL) components.**
There is **no ambition to develop new features**. Maintenance is manual: when
something drifts out of date, you bump a version, re-verify the app, and commit.
Do not add features, refactor, or "improve" things while doing maintenance —
small, focused changes only.
> Scope guard: this repo is a local fork of `macrodev8/ScanSlicer`, published at
> `https://code.dgerdesmann.de/dan/ScanSlicer.git`. License is GPL v3. Keep the
> upstream credit and the "inspired by PhotoSlicer" note intact. See `README.md`
> and `docs/` for project context.
---
## Where things live
| Component | File(s) |
|---|---|
| Package metadata + dependency constraints | `pyproject.toml` |
| **Pinned** dependency versions (source of truth) | `uv.lock` |
| Runtime container image | `Dockerfile` (ubuntu:24.04 + uv) |
| AppImage recipe + build scripts | `packaging/appimage/` |
| Bundled third-party license notice | `THIRD_PARTY_NOTICES` |
| Dev env (Python 3.12) | `.venv/` (managed by uv) |
Dependency management is done with **uv**. `pyproject.toml` declares the
requirements (mostly loose), and `uv.lock` pins the *exact* versions that get
installed and shipped. `uv.lock` is committed — do not edit it by hand; let uv
rewrite it.
---
## Code invariants (orientation fix + bbox order)
This fork's one real code change is the mirrored-export orientation fix. Keep
these invariants in mind when touching geometry code.
- **`PhotoSlice.bbox` is a 4×2 numpy array in the order
`[top-left, top-right, bottom-right, bottom-left]`.** `Autoslicer.save_slice()`
(`autoslicer.py`) builds the perspective transform assuming that order. Do NOT
change this contract without updating every consumer.
- **`PhotoSlice.swapped`** (default `False`) is the persistent "user explicitly
mirrored left/right" flag. `swap_left_right()` toggles it and reindexes
corners `[TL,TR,BR,BL] -> [TR,TL,BL,BR]` (`bbox[[1,0,3,2]]`) — a pure
left/right mirror that keeps the top and bottom edges in place.
- **Root cause of the original bug:** `normalize_top_left()` (`slices.py`)
re-derives "top" from geometry, which is ambiguous, and `squarize()` /
`reset_rotation()` both re-run it at the end, silently overwriting manual
corrections. The fix makes `normalize_top_left()` apply the same `[1,0,3,2]`
reindex when `swapped` is true, so manual orientation survives. (Historical:
an earlier `[0,3,2,1]` reindex was wrong — it rotated the quad 90°.)
- **UI wiring:** "Swap L/R" button (`ui_builders_preview.py`) →
`CanvasStateMixin.swap_active_slice_left_right()` (`canvas_state.py`) →
`ScanSlicerCanvasActionsMixin.swap_active_box_left_right()`
(`controller_canvas_actions.py`) → `PhotoSlice.swap_left_right()`.
- **Visual color coding** (`canvas_render.py`): corners — left = orange, right
= cyan; edges — red = top (0→1), green = right (1→2), orange = bottom (2→3),
left edge (3→0) follows lock state (blue locked / lightgreen unlocked).
---
## Current pinned versions (snapshot)
Update this table whenever you bump something, so the next session has a
starting point.
| Component | Pinned version | Where |
|---|---|---|
| PySide6 / PySide6-Addons / -Essentials / shiboken6 | 6.10.2 | `uv.lock` |
| opencv-python-headless | 4.13.0.92 | `uv.lock` (constraint `~=4.13.0` in `pyproject.toml`) |
| numpy | 2.2.6 (Py<3.11) / 2.4.2 (Py≥3.11) | `uv.lock` |
| Python (dev env) | 3.12 | `.venv` |
| Python floor | `>=3.10` | `pyproject.toml` |
| uv (both Dockerfiles) | 0.10.2 | `Dockerfile`, `packaging/appimage/Dockerfile` |
| appimage-builder | 1.1.0 | `packaging/appimage/Dockerfile` |
| AppImage base OS | Ubuntu **noble** (24.04) | `packaging/appimage/AppImageBuilder.yml` |
| Runtime image base | Ubuntu 24.04 | `Dockerfile` |
---
## What can go stale, and the priority order
Review these roughly in this order — the first items are the real security/EOL
surface; the last ones are packaging plumbing that only matters at build time.
1. **PySide6 / Qt** — the largest attack surface and the component with the
most aggressive EOL cadence. Qt 6.x minor releases stop receiving updates
quickly; PySide6 tracks Qt. Keep this on a recent 6.x.
2. **opencv-python-headless** — image processing; bump to the latest 4.x.
3. **numpy** — bump to the latest 2.x.
4. **Python** — the *shipped* runtime is what matters. The AppImage bundles its
own Python (from Ubuntu noble, currently 3.12), so the app does not run on
the host's Python. The `requires-python = ">=3.10"` floor only affects
development resolution. Watch Python 3.10's EOL: when it goes EOL, raise the
floor (e.g. to `>=3.11`) so no EOL interpreter is ever considered valid.
5. **Packaging toolchain** — `uv` and `appimage-builder` versions pinned in the
two Dockerfiles. Only bump these when you have a reason (build breaks, or a
notable security fix in the tool itself).
6. **Ubuntu base images** — `noble` is an LTS (supported to ~2029). Revisit
the apt source and GPG key in `AppImageBuilder.yml` and the `FROM` line in
both Dockerfiles when noble approaches EOL.
Suggested cadence: check PyPI/upstream releases every ~3–6 months, or promptly
when a security advisory is published for any of the above.
---
## Updating Python dependencies
All commands run from the repo root. The dev env is `.venv` (Python 3.12).
### 1. Bring the environment up to the committed lock (baseline)
```bash
uv sync --frozen
```
### 2. Bump a single dependency to its latest
```bash
uv lock --upgrade-package PySide6
uv lock --upgrade-package opencv-python-headless
uv lock --upgrade-package numpy
```
`--dry-run` previews the change without touching anything:
```bash
uv lock --upgrade-package PySide6 --dry-run
```
To bump *everything* at once (use sparingly — harder to bisect if something
breaks):
```bash
uv lock --upgrade
```
### 3. Install the updated set and verify
```bash
uv sync
uv run scanslicer /path/to/test/scans
```
Then run the smoke-test checklist below.
### Special case: opencv major/minor bump
`opencv-python-headless` is the **only** dependency with a constraint in
`pyproject.toml` (`~=4.13.0`), which pins it to the 4.13.x line. To move to
4.14+, edit that constraint in `pyproject.toml` first, then run
`uv lock --upgrade-package opencv-python-headless`. PySide6 and numpy are
unpinned in `pyproject.toml`, so uv can move them freely.
### After any version change
- Commit `pyproject.toml` (if edited) **and** `uv.lock` together.
- Update the version table above.
- Update `THIRD_PARTY_NOTICES` — it lists concrete PySide6/Qt, numpy, and
opencv versions, and it ships inside every AppImage. Keep it aligned with
`uv.lock`.
- Rebuild and smoke-test the AppImage if the runtime stack (PySide6/Qt) changed.
---
## Updating the AppImage packaging toolchain
The AppImage build pins its own tool versions in `packaging/appimage/Dockerfile`:
- `uv` (`COPY --from=ghcr.io/astral-sh/uv:0.10.2 ...`)
- `appimage-builder==1.1.0` (an `ARG` near the top)
Bump either by editing that file. If you change `appimage-builder`, the
`AppImageBuilder.yml` recipe may need adjusting for new versions (rare). The
Ubuntu apt source and GPG key live in `AppImageBuilder.yml` — update those when
noble goes EOL or the key rotates.
> **Hardcoded Python paths:** `AppImageBuilder.yml` hardcodes `python3.12` in
> `QT_PLUGIN_PATH` and `LD_LIBRARY_PATH`. If the AppImage base OS ever moves to
> a different Python minor, those two lines must be updated to match.
---
## Updating the runtime Docker image
`Dockerfile` (repo root) pins the base (`ubuntu:24.04`) and `uv 0.10.2`.
Bump these when needed, then rebuild and run the container smoke test:
```bash
docker compose up --build
```
---
## Verifying a change (smoke-test checklist)
There is **no automated test suite**. After any dependency or packaging change,
manually confirm the app still works:
1. **Startup** on both Wayland and X11:
```bash
uv run scanslicer --backend wayland
uv run scanslicer --backend x11
```
2. **Detection** on a representative sample scan (boxes appear on load/Detect).
3. **Save flow** — lock a box, export, confirm the output is straight and not
mirrored (this fork's orientation fix is the thing most likely to regress).
4. **Auto-tune** — run a profile and confirm feedback is written/read without
error.
5. If the runtime stack changed: rebuild the AppImage and confirm it launches
and saves.
6. If the Docker image changed: `docker compose up --build` and confirm the app
opens.
---
## The Docker gotcha (recap — keep this in mind)
The maintainer's Docker points at **Docker Desktop** (`desktop-linux` context).
Its bind-mount filesystem forbids `chown`, but `appimage-builder` needs root
ownership while extracting `.deb` packages, so the Dockerized AppImage build
fails with:
```
dpkg-deb ... Cannot change ownership to uid 0, gid 0: Operation not permitted
```
Workaround: run the build against the **native** daemon instead:
```bash
DOCKER_HOST=unix:///var/run/docker.sock bash packaging/appimage/build-appimage-docker.sh
```
(or `docker context use default` first). Output lands in
`appimage/dist/ScanSlicer-<version>-x86_64.AppImage` (the `appimage/` dir is
git-ignored).
---
## Commit / release hygiene
- Commit `uv.lock` and `pyproject.toml` (and `THIRD_PARTY_NOTICES` when
applicable) together as a single change.
- Write a short, factual commit message, e.g. `Bump PySide6 to 6.10.x`.
- Push to the Forgejo remote: `git push origin`.
- The AppImage build verifies package hashes (`--require-hashes`) from
`uv.lock`; publishing the AppImage alongside the matching committed source is
already handled by committing before building.
---
## One-line summary
Bump PySide6/Qt first and most often; bump opencv and numpy when convenient;
treat uv/appimage-builder/Ubuntu as build plumbing you only touch when
necessary. After every change, run the smoke-test checklist, rebuild the
AppImage if the runtime stack moved, and commit `uv.lock` + `pyproject.toml`
together.