docs: add packaging design spec + decision log
Captures the brainstormed design for sethLabels' packaging pipeline: - Strict-zero source patches; all sethLabels content in NEW top-level dirs - Linux artifacts: .deb (CPack-driven) + AppImage (linuxdeploy + Qt plugin), both built from upstream's existing install() rules - macOS: Homebrew tap, build-from-source — no macOS CI, no signing, no Apple Developer ID - Build infra: manual local builds during battle-test phase; shell scripts under scripts/ are the canonical recipe (CI YAML at the eventual public-flip on GitHub will call them verbatim) - Versioning: <upstream-tag>-seth<N> for clear lineage + rebuild counter - Package name: glabels-qt (matches binary, brew formula, command) Spec at sethlabels-docs/specs/2026-04-29-packaging-design.md. Decision log at DECISIONS.md. No code introduced — design only. Plan-writing follows.
This commit is contained in:
@@ -6,7 +6,35 @@ Format: `YYYY-MM-DD: <decision> — <why>`
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
- **2026-04-29: Strict-zero source patches.** Never edit any upstream-tracked file. All sethLabels content lives in new files in new top-level directories at the repo root. — Makes `git pull upstream master --ff-only` succeed forever; rebase friction = 0. Allowlist exception: `.gitignore` (one-time scaffold-time touch). Enforced by `scripts/check-no-upstream-edits.sh`. See spec §I1, §D5.
|
||||||
|
- **2026-04-29: Eventual-public fork.** sethLabels stays on Gitea during battle-test, then promotes to a formally public fork on GitHub once the build/release pipeline is solid. — Drives spec invariants: build host must be a clean Debian 13 / Ubuntu LTS box (not steel141-specific); brew tap source URL must be cleanly flippable Gitea→GitHub; CI is added at the flip, not before.
|
||||||
|
- **2026-04-29: Linux artifacts = `.deb` + AppImage; macOS via Homebrew tap (build from source).** — `.deb` for Debian-family install ergonomics, AppImage for portability to non-Debian Linux. Brew tap eliminates macOS CI/signing/$99 Apple Dev ID entirely; users' Macs build from source. See spec §D1, §D2.
|
||||||
|
- **2026-04-29: Build infrastructure = manual local builds during battle-test.** Shell scripts under `scripts/` are the canonical build recipe; CI YAML at the public-flip will call those scripts unmodified. — Local feedback loop beats CI loop during iteration on packaging. Defers infra cost. See spec §I3, §D3.
|
||||||
|
- **2026-04-29: Versioning = `<upstream-tag>-seth<N>`.** E.g., `3.99-master618-seth1`. — Lineage-preserving; rebuild counter survives packaging-only fixes; sorts correctly under `dpkg --compare-versions`. See spec §D4.
|
||||||
|
- **2026-04-29: Package name = `glabels-qt`.** Same name as upstream binary, brew formula, and the command users run. — sethLabels identity lives at repo level + version-string `-seth<N>` marker, not in the package name. Avoids confusing identity-split between package name and binary name. See spec §D6.
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
|
- **2026-04-29: macOS = no CI required.** Homebrew tap with build-from-source means no macOS runner, no Apple Developer ID, no notarization pipeline. Tap repo (`git.sethpc.xyz/Seth/homebrew-tap`) is one ~30-line Ruby file. — Major simplification; Mac users build locally on first install (~5–10 min), all subsequent installs are version pin bumps to the tap formula.
|
||||||
|
- **2026-04-29: Debian-family is the install target, not steel141.** Build host happens to be steel141 (or any clean Debian/Ubuntu box); install target is generic Debian-family. — No homelab paths, hostnames, or assumptions in scripts. Reproducibility on a fresh VM is the bar.
|
||||||
|
- **2026-04-29: Spec, scripts, and packaging metadata live in NEW top-level dirs.** `sethlabels-docs/`, `scripts/`, `packaging/`. NOT in upstream's `docs/` directory. — Clear fork boundary; preserves strict-zero spirit (don't pollute upstream namespaces with our content).
|
||||||
|
|
||||||
## Deferred / Rejected
|
## Deferred / Rejected
|
||||||
<!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas -->
|
<!-- Decisions NOT to do something are just as valuable -- prevents re-proposing rejected ideas -->
|
||||||
|
|
||||||
|
- **Rejected 2026-04-29: AppImage-only Linux distribution.** — Loses native Debian package manager integration (`apt remove`, dependency tracking) on the primary target distros.
|
||||||
|
- **Rejected 2026-04-29: `.deb`-only Linux distribution.** — Loses portability to non-Debian Linux (RHEL/Fedora/Arch users couldn't install without recompiling).
|
||||||
|
- **Rejected 2026-04-29: Flatpak.** — Sandbox runtime requirement + manifest complexity overkill for a personal-use packaging fork.
|
||||||
|
- **Rejected 2026-04-29: Signed + notarized macOS `.dmg` (Apple Developer ID).** — $99/year recurring + notarization CI complexity not justified for a build-from-source Homebrew alternative.
|
||||||
|
- **Rejected 2026-04-29: Unsigned macOS `.dmg`.** — Gatekeeper friction (right-click → Open) is a poor first-run experience compared to brew install.
|
||||||
|
- **Rejected 2026-04-29: Gitea Actions self-hosted runner during battle-test.** — Homelab CT spin-up + workflow YAML setup not justified before public-flip; manual builds are faster to iterate on.
|
||||||
|
- **Rejected 2026-04-29: Public GitHub Actions from day one.** — Skips the "battle-test in private" intent; want to validate the pipeline before going public.
|
||||||
|
- **Rejected 2026-04-29: Plain upstream-tag versioning (no `-seth<N>` marker).** — No way to distinguish v1 of the `.deb` from v2 after a packaging fix; would have to lie about which upstream commit the package contains.
|
||||||
|
- **Rejected 2026-04-29: Independent semver (`0.1.0`, `0.2.0`).** — Loses upstream-lineage info from the version string; users couldn't tell which glabels-qt commit they have without checking the changelog.
|
||||||
|
- **Rejected 2026-04-29: Date-based versioning (`2026.04.29`).** — Loses both upstream-lineage and rebuild-counter information.
|
||||||
|
- **Rejected 2026-04-29: Permissive small-patches policy on upstream files.** — Creates rebase friction on each `git pull upstream master`; even a one-file CMakeLists carve-out turned out to be unnecessary since CPack `-D` flags cover all needed metadata at build time.
|
||||||
|
- **Rejected 2026-04-29: Package name `sethlabels`.** — Strict-zero policy forbids renaming the executable, so `apt install sethlabels` then running `glabels-qt` would split package identity from binary identity. Confusing.
|
||||||
|
- **Deferred: Windows packaging.** — Per project brief. Upstream's NSIS support is intact; can be revisited later. Not blocking macOS+Linux delivery.
|
||||||
|
- **Deferred: Custom default templates baked into the package.** — Strict-zero forbids; user-specific templates can live in `~/.config/glabels-qt/templates/` or a separate repo if/when needed.
|
||||||
|
- **Deferred: Branding, icon, splash, or string changes.** — Strict-zero forbids. sethLabels is a packaging fork, not a rebrand.
|
||||||
|
- **Deferred: Distribution to Debian backports / PPA / Ubuntu universe.** — Requires Debian Developer mentorship + ongoing policy compliance work; not justified for current scope.
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
# sethLabels Packaging — Design Spec
|
||||||
|
|
||||||
|
- **Status:** approved (brainstormed 2026-04-29, awaiting user review of this document before plan-writing)
|
||||||
|
- **Author:** Seth Freiberg, with assistance from Claude Code
|
||||||
|
- **Audience:** anyone implementing or maintaining sethLabels' packaging/release pipeline
|
||||||
|
- **Project:** [git.sethpc.xyz/Seth/sethLabels](https://git.sethpc.xyz/Seth/sethLabels)
|
||||||
|
- **Upstream:** [github.com/j-evins/glabels-qt](https://github.com/j-evins/glabels-qt) (the glabels.org team)
|
||||||
|
|
||||||
|
## 1. Problem & purpose
|
||||||
|
|
||||||
|
The upstream glabels-qt project explicitly does not publish self-hosted binary releases ("Currently there are no self-hosted binary snapshot releases available… I encourage you to try building the code yourself" — upstream README). sethLabels exists to fill exactly that gap: produce installable binary artifacts for Debian-family Linux and macOS without changing the upstream application itself.
|
||||||
|
|
||||||
|
The eventual goal is to publish sethLabels as a formally public packaging-fork on GitHub once the build/release pipeline is battle-tested. This spec describes the pipeline and discipline that make that possible.
|
||||||
|
|
||||||
|
## 2. Invariants (load-bearing)
|
||||||
|
|
||||||
|
These are not guidelines — they define the project's shape. Violating any of them moves sethLabels toward "real fork" territory and out of "deployment fork."
|
||||||
|
|
||||||
|
| # | Invariant | Why |
|
||||||
|
|---|-----------|-----|
|
||||||
|
| I1 | **Strict zero source patches.** No upstream file is ever edited. Every sethLabels addition lives in NEW files in NEW top-level directories at the repo root. | Makes `git pull --rebase upstream master` always succeed without conflicts (our scaffold commit re-applies cleanly on each new upstream base). Rebase friction = 0. |
|
||||||
|
| I2 | **Debian-family + macOS-via-brew target only.** No homelab-specific paths, hostnames, or assumptions anywhere in scripts. | Build must be reproducible on a clean Debian 13 / Ubuntu LTS VM. Public-fork future depends on this. |
|
||||||
|
| I3 | **Shell scripts under `scripts/` are the canonical build recipe.** CI YAML (when added later at the public-flip) calls these scripts verbatim — no logic in YAML. | Prevents recipe drift between manual builds and CI. The script is the truth. |
|
||||||
|
| I4 | **Manual local builds during battle-test phase.** No CI infrastructure (Gitea Actions, GitHub Actions, self-hosted runner) until the public-flip on GitHub. | Defers infra cost. Iteration speed > automation during exploration. |
|
||||||
|
| I5 | **No branding, icon, splash, or string changes.** sethLabels is a packaging fork, not a rebrand. Artifacts identify as `glabels-qt`. | Per IDEA.md ("branding/customization: minimal — this is for personal use, not a product") and strict-zero. |
|
||||||
|
|
||||||
|
The one exception to I1: `.gitignore` carries an appended sethLabels section. This was a one-time touch made at scaffold time and is treated as a known allowlisted exception throughout this spec.
|
||||||
|
|
||||||
|
## 3. Decisions (settled, with rationale)
|
||||||
|
|
||||||
|
Each decision below was the conclusion of a multiple-choice round during brainstorming. Recorded here so future maintainers can see what alternatives were considered and why the current choice won. See `DECISIONS.md` for the same list in shorter form.
|
||||||
|
|
||||||
|
### D1 — Linux distribution formats: `.deb` AND AppImage
|
||||||
|
|
||||||
|
- **Why:** `.deb` for Debian-family targets (clean apt-managed install/upgrade/uninstall); AppImage as portable fallback for any-Linux-anywhere. AppImage is essentially free once `.deb` works since linuxdeploy reuses upstream's `install()` rules.
|
||||||
|
- **Rejected:** AppImage-only (loses native package manager integration), `.deb`-only (loses portability to non-Debian Linux), Flatpak (heavyweight runtime requirement, overkill for this use case).
|
||||||
|
- **Cost:** AppImage bundling can be finicky around Qt6 plugin discovery — see §8.2 mitigation.
|
||||||
|
|
||||||
|
### D2 — macOS distribution: Homebrew tap, build-from-source
|
||||||
|
|
||||||
|
- **Why:** zero macOS CI requirement (brew runs the build on the user's machine), no $99/yr Apple Developer ID, no notarization pipeline complexity, brew handles Qt6 dependency resolution automatically (`depends_on "qt@6"`).
|
||||||
|
- **Rejected:** unsigned `.dmg` (Gatekeeper friction), signed+notarized `.dmg` (annual cost + CI complexity), tap-with-pre-built-cask (still requires building somewhere, defeats the simplification).
|
||||||
|
- **Constraint accepted:** first-time install on a Mac takes ~5–10 min (cmake build of Qt app from source). Acceptable for a technical user, which is the audience.
|
||||||
|
- **Tap repo:** lives separately at `git.sethpc.xyz/Seth/homebrew-tap`. Not in the sethLabels repo.
|
||||||
|
|
||||||
|
### D3 — Build infrastructure: manual local builds during battle-test
|
||||||
|
|
||||||
|
- **Why:** local feedback loop (~2 min) beats CI feedback loop (push + wait); during iteration on packaging recipes the local loop is essential. Scripts written for manual use translate to CI workflow YAML mechanically when needed.
|
||||||
|
- **Rejected:** Gitea Actions self-hosted runner (homelab CT spin-up cost not justified pre-public), public GitHub Actions from day one (skips the "battle-test in private" intent).
|
||||||
|
- **Forward path:** at public-flip on GitHub, scripts get wrapped in a `.github/workflows/release.yml` that calls them — no logic moves into YAML.
|
||||||
|
- **Build host expectation:** scripts must run on a clean Debian 13 / Ubuntu LTS box. Steel141 is acceptable as a build host but is not the target audience for resulting binaries.
|
||||||
|
|
||||||
|
### D4 — Versioning: `<upstream-tag>-seth<N>`
|
||||||
|
|
||||||
|
- **Why:** lineage in the version answers "which upstream am I running?" without changelog spelunking. Rebuild counter (`seth1` → `seth2`) lets us ship a packaging-only fix without lying about which upstream code is inside. Sorts correctly under `dpkg --compare-versions`.
|
||||||
|
- **Format:** `<upstream-tag>` = output of `git describe --tags --abbrev=0 upstream/master` (e.g., `3.99-master618`). `<N>` = count of existing `<upstream-tag>-seth*` tags + 1.
|
||||||
|
- **Rejected:** plain upstream tag (no way to distinguish v1 of the .deb from v2 after a packaging fix), independent semver (loses upstream-lineage info), date-based (loses both lineage and rebuild-counter info).
|
||||||
|
|
||||||
|
### D5 — Upstream-touch policy: strict zero
|
||||||
|
|
||||||
|
- **Why:** the IDEA brief explicitly calls for "diff small enough to rebase periodically." Strict zero takes that to its limit. CPack metadata is overridable via `cpack -D KEY=VALUE` flags at build time — no source edit required to package. Any future need for a CMake gap should be upstreamed as a PR to glabels-qt rather than carried as a local patch.
|
||||||
|
- **Rejected:** permissive small-patches (creates rebase friction), strict-zero with a CMakeLists carve-out (the carve-out is not actually needed, since CPack `-D` covers everything we want).
|
||||||
|
- **Enforcement:** `scripts/check-no-upstream-edits.sh` runs as a guardrail; allowlist = `.gitignore` only.
|
||||||
|
|
||||||
|
### D6 — Package name: `glabels-qt`
|
||||||
|
|
||||||
|
- **Why:** consistent muscle memory across `.deb`, AppImage, brew formula, and the binary the user runs. The "sethLabels" identity lives at the repo level + in the version string's `-seth<N>` marker, not in the package name. Future-proofs the public-fork narrative as "yet another packaging fork of glabels-qt."
|
||||||
|
- **Rejected:** `sethlabels` (would split identity from binary name, since strict-zero means we can't rename the executable; `apt install sethlabels` then `glabels-qt` is confusing).
|
||||||
|
- **Risk acknowledged:** if Debian later ships an official `glabels-qt` package, our sethLabels-versioned variant would still dominate by version-sort (`3.99-master618-seth1` > `3.99`-something-debian). Re-evaluate at that time.
|
||||||
|
|
||||||
|
## 4. Repo layout
|
||||||
|
|
||||||
|
The post-spec layout. Existing entries reflect what's already in the repo at `2026-04-29`. NEW entries are introduced by the spec implementation.
|
||||||
|
|
||||||
|
```
|
||||||
|
sethLabels/
|
||||||
|
├── (all upstream files — strict zero, never touched)
|
||||||
|
│ ├── glabels/, glabels-batch/, model/, backends/, ...
|
||||||
|
│ ├── CMakeLists.txt, README.md, LICENSE, ...
|
||||||
|
│ ├── data/, templates/, translations/, user-docs/
|
||||||
|
│ └── docs/ ← upstream's docs dir, never written into
|
||||||
|
│
|
||||||
|
├── .gitignore ← I1 exception: sethLabels section appended
|
||||||
|
│
|
||||||
|
├── CLAUDE.md ← already present (project instructions)
|
||||||
|
├── IDEA.md ← already present (plain-language brief)
|
||||||
|
├── DECISIONS.md ← already present (decision log)
|
||||||
|
├── GITEA_API.md ← already present (gitignored symlink)
|
||||||
|
├── README.sethlabels.md ← NEW: fork purpose, install/build entry
|
||||||
|
│
|
||||||
|
├── scripts/ ← NEW: canonical build recipe (I3)
|
||||||
|
│ ├── README.md ← human-readable run instructions
|
||||||
|
│ ├── build-deb.sh ← entry: builds the .deb
|
||||||
|
│ ├── build-appimages.sh ← entry: builds the 2 AppImages
|
||||||
|
│ ├── compute-version.sh ← emits "<upstream-tag>-seth<N>"
|
||||||
|
│ ├── check-no-upstream-edits.sh ← guardrail enforcing I1
|
||||||
|
│ └── lib/
|
||||||
|
│ ├── deps-debian.sh ← apt-installable build deps
|
||||||
|
│ └── linuxdeploy.sh ← downloads/caches linuxdeploy + qt plugin
|
||||||
|
│
|
||||||
|
├── packaging/ ← NEW: data files consumed by scripts
|
||||||
|
│ ├── deb-metadata.env ← maintainer, section, depends fallback
|
||||||
|
│ ├── appimage-recipe.env ← linuxdeploy plugin allowlist, exclude list
|
||||||
|
│ └── changelog.md ← human-readable per-release notes
|
||||||
|
│
|
||||||
|
├── sethlabels-docs/ ← NEW: sethLabels' own docs (this spec lives here)
|
||||||
|
│ └── specs/
|
||||||
|
│ └── 2026-04-29-packaging-design.md ← THIS file
|
||||||
|
│
|
||||||
|
└── .claude/ ← already present
|
||||||
|
└── handoffs/
|
||||||
|
└── 2026-04-29-125823-scaffold-only.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Companion repository (separate Gitea repo, not part of sethLabels):
|
||||||
|
|
||||||
|
```
|
||||||
|
homebrew-tap/ ← git.sethpc.xyz/Seth/homebrew-tap
|
||||||
|
├── README.md
|
||||||
|
└── Formula/
|
||||||
|
└── glabels-qt.rb ← brew formula
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Build pipeline
|
||||||
|
|
||||||
|
### 5.1 Build host requirements
|
||||||
|
|
||||||
|
Either a clean Debian 13 (Trixie) or Ubuntu 24.04 LTS box. The build host produces both the `.deb` and the AppImage artifacts; one machine, two scripts.
|
||||||
|
|
||||||
|
`scripts/lib/deps-debian.sh` is the single source of truth for build dependencies and prints an actionable `apt install …` command when something is missing. Expected dependency set:
|
||||||
|
|
||||||
|
```
|
||||||
|
build-essential cmake ninja-build pkg-config
|
||||||
|
qt6-base-dev qt6-base-dev-tools qt6-svg-dev qt6-tools-dev qt6-tools-dev-tools
|
||||||
|
qt6-l10n-tools libqt6printsupport6 libqt6svg6 libqt6widgets6 libqt6xml6 libqt6gui6
|
||||||
|
libqt6concurrent6 libqt6core6 libqt6test6
|
||||||
|
zlib1g-dev libqrencode-dev libzint-dev libgnubarcode-dev
|
||||||
|
file dpkg-dev fakeroot
|
||||||
|
wget # for downloading linuxdeploy on first AppImage build
|
||||||
|
```
|
||||||
|
|
||||||
|
`linuxdeploy` and `linuxdeploy-plugin-qt` are downloaded by `scripts/lib/linuxdeploy.sh` to a script-local cache (e.g., `scripts/.cache/linuxdeploy-x86_64.AppImage`) on first run. They are not apt-installable on Debian/Ubuntu, hence the bootstrap step. Cache is gitignored.
|
||||||
|
|
||||||
|
### 5.2 `scripts/build-deb.sh`
|
||||||
|
|
||||||
|
Sequential steps. Any non-zero exit aborts the build with a clear error message.
|
||||||
|
|
||||||
|
1. **Sanity check the build host:** confirm Debian-family (`/etc/os-release` ID is `debian` or `ubuntu`), confirm all `deps-debian.sh` packages are installed.
|
||||||
|
2. **Strict-zero guardrail:** call `check-no-upstream-edits.sh`. If any tracked-upstream file diverges from `upstream/master` (allowlist: `.gitignore`), abort.
|
||||||
|
3. **Compute version:** `VERSION=$(./scripts/compute-version.sh)`.
|
||||||
|
4. **Out-of-tree build:**
|
||||||
|
```
|
||||||
|
cmake -S . -B build/deb -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build build/deb --parallel
|
||||||
|
```
|
||||||
|
5. **Run CPack with overrides:**
|
||||||
|
```
|
||||||
|
cd build/deb && cpack -G DEB \
|
||||||
|
-D CPACK_PACKAGE_VERSION="$VERSION" \
|
||||||
|
-D CPACK_DEBIAN_PACKAGE_MAINTAINER="$(. ../../packaging/deb-metadata.env && echo "$MAINTAINER")" \
|
||||||
|
-D CPACK_DEBIAN_PACKAGE_SECTION="graphics" \
|
||||||
|
-D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON \
|
||||||
|
-D CPACK_DEBIAN_PACKAGE_HOMEPAGE="https://glabels.org" \
|
||||||
|
-D CPACK_DEBIAN_FILE_NAME=DEB-DEFAULT
|
||||||
|
```
|
||||||
|
`CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON` runs `dpkg-shlibdeps` against built binaries to compute exact runtime library deps automatically. This avoids hand-maintaining a Depends list.
|
||||||
|
6. **Smoke test the artifact:**
|
||||||
|
- `dpkg-deb --info build/deb/glabels-qt_${VERSION}_amd64.deb` — verifies parse-ability.
|
||||||
|
- `dpkg-deb --contents …` checked for both `/usr/bin/glabels-qt` and `/usr/bin/glabels-batch-qt`.
|
||||||
|
- `lintian build/deb/glabels-qt_${VERSION}_amd64.deb` if available — warnings noted but not fatal during battle-test.
|
||||||
|
7. **Artifact path printed for the operator** to upload: `build/deb/glabels-qt_${VERSION}_amd64.deb`.
|
||||||
|
|
||||||
|
### 5.3 `scripts/build-appimages.sh`
|
||||||
|
|
||||||
|
1. Same sanity / guardrail / version-compute steps as 5.2.
|
||||||
|
2. **Out-of-tree build into `build/appimage`:**
|
||||||
|
```
|
||||||
|
cmake -S . -B build/appimage -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr
|
||||||
|
cmake --build build/appimage --parallel
|
||||||
|
```
|
||||||
|
3. **Stage install tree to `AppDir`:**
|
||||||
|
```
|
||||||
|
DESTDIR=$(pwd)/build/appimage/AppDir cmake --install build/appimage
|
||||||
|
```
|
||||||
|
`AppDir/usr/bin/glabels-qt` and `AppDir/usr/bin/glabels-batch-qt` exist after this step (driven by upstream's existing `install()` rules).
|
||||||
|
4. **Bundle GUI AppImage:**
|
||||||
|
```
|
||||||
|
linuxdeploy --appdir build/appimage/AppDir --plugin qt \
|
||||||
|
--executable build/appimage/AppDir/usr/bin/glabels-qt \
|
||||||
|
--desktop-file build/appimage/AppDir/usr/share/applications/org.glabels.glabels-qt.desktop \
|
||||||
|
--icon-file build/appimage/AppDir/usr/share/icons/hicolor/scalable/apps/glabels.svg \
|
||||||
|
--output appimage
|
||||||
|
mv glabels-qt-x86_64.AppImage sethlabels-gui-${VERSION}-x86_64.AppImage
|
||||||
|
```
|
||||||
|
5. **Bundle Batch AppImage** — re-stage AppDir (or use a separate `AppDir-batch`) to avoid pulling GUI-only Qt plugins into the batch bundle:
|
||||||
|
```
|
||||||
|
linuxdeploy --appdir build/appimage/AppDir-batch --plugin qt \
|
||||||
|
--executable build/appimage/AppDir-batch/usr/bin/glabels-batch-qt \
|
||||||
|
--output appimage
|
||||||
|
mv glabels-batch-qt-x86_64.AppImage sethlabels-batch-${VERSION}-x86_64.AppImage
|
||||||
|
```
|
||||||
|
Note: the batch CLI doesn't need a desktop file or icon (no GUI launcher).
|
||||||
|
6. **Smoke tests:**
|
||||||
|
- `./sethlabels-batch-${VERSION}-x86_64.AppImage --version` — must exit 0 and print a version string.
|
||||||
|
- `QT_QPA_PLATFORM=minimal ./sethlabels-gui-${VERSION}-x86_64.AppImage --help` — must exit 0 (validates that the Qt plugin set is complete enough to start a headless Qt event loop).
|
||||||
|
7. **Artifact paths printed for the operator** to upload.
|
||||||
|
|
||||||
|
### 5.4 `scripts/compute-version.sh`
|
||||||
|
|
||||||
|
Pure logic, no side effects. Emits version string to stdout.
|
||||||
|
|
||||||
|
```
|
||||||
|
upstream_tag=$(git describe --tags --abbrev=0 upstream/master)
|
||||||
|
existing_count=$(git tag --list "${upstream_tag}-seth*" | wc -l)
|
||||||
|
next_n=$((existing_count + 1))
|
||||||
|
echo "${upstream_tag}-seth${next_n}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caller responsibility:** the local tag db must be fresh before running this script — `git tag --list` reads local refs only. The release flow (§6 step 1) does `git fetch --all --tags` to satisfy this. If invoked outside the release flow, the caller must run `git fetch origin --tags` first or risk a stale `<N>` value.
|
||||||
|
|
||||||
|
The script is idempotent: running it multiple times before a new tag is created always returns the same value. Both `build-deb.sh` and `build-appimages.sh` call this internally to embed the version into their artifacts. Step 6 of the release flow re-invokes it to obtain `$VERSION` for the `git tag` command — values agree by purity.
|
||||||
|
|
||||||
|
### 5.5 `scripts/check-no-upstream-edits.sh`
|
||||||
|
|
||||||
|
The script must catch BOTH committed and uncommitted edits to upstream files. Two checks:
|
||||||
|
|
||||||
|
1. **Committed drift** — files changed in commits unique to `HEAD` vs. `upstream/master`:
|
||||||
|
```
|
||||||
|
committed=$(git diff --name-only upstream/master..HEAD)
|
||||||
|
```
|
||||||
|
2. **Working-tree drift** — uncommitted local edits to tracked files:
|
||||||
|
```
|
||||||
|
working=$(git diff --name-only HEAD)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both lists are filtered against the same allowlist. Any path NOT matching is a violation.
|
||||||
|
|
||||||
|
```
|
||||||
|
allowed_pattern='^\.gitignore$|^\.claude/|^scripts/|^packaging/|^sethlabels-docs/|^README\.sethlabels\.md$|^CLAUDE\.md$|^IDEA\.md$|^DECISIONS\.md$'
|
||||||
|
all_changes=$(printf "%s\n%s\n" "$committed" "$working" | sort -u | sed '/^$/d')
|
||||||
|
violations=$(echo "$all_changes" | grep -vE "$allowed_pattern" || true)
|
||||||
|
if [ -n "$violations" ]; then
|
||||||
|
echo "ERROR: strict-zero policy violated. The following upstream files have been modified:"
|
||||||
|
echo "$violations"
|
||||||
|
echo "See sethlabels-docs/specs/2026-04-29-packaging-design.md §I1."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit 0 silently on clean state.
|
||||||
|
|
||||||
|
Allowlist note: `GITEA_API.md` is intentionally NOT in the allowlist because it is `.gitignore`-excluded and will never appear in any git diff. Including it would be dead code.
|
||||||
|
|
||||||
|
## 6. Release flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. git fetch --all --tags (refreshes upstream commits AND local tag db, which compute-version.sh reads)
|
||||||
|
2. git rebase upstream/master (strict-zero ⇒ always succeeds without conflicts; scaffold commit re-applies on new base)
|
||||||
|
3. ./scripts/check-no-upstream-edits.sh (guardrail; aborts on drift)
|
||||||
|
4. ./scripts/build-deb.sh (~2 min)
|
||||||
|
5. ./scripts/build-appimages.sh (~5 min, two AppImages)
|
||||||
|
6. VERSION=$(./scripts/compute-version.sh)
|
||||||
|
7. git tag "$VERSION"
|
||||||
|
8. git push origin main --tags
|
||||||
|
9. Create Gitea release for tag $VERSION via gitea CLI / API; attach:
|
||||||
|
- build/deb/glabels-qt_${VERSION}_amd64.deb
|
||||||
|
- sethlabels-gui-${VERSION}-x86_64.AppImage
|
||||||
|
- sethlabels-batch-${VERSION}-x86_64.AppImage
|
||||||
|
10. Edit ../homebrew-tap/Formula/glabels-qt.rb:
|
||||||
|
- bump tag pin to $VERSION
|
||||||
|
- update sha256 of the source tarball
|
||||||
|
- commit "bump glabels-qt to $VERSION", push
|
||||||
|
11. Smoke verify: download .deb on a fresh Debian 13 VM, install with `apt install ./...`, run `glabels-qt --version`.
|
||||||
|
```
|
||||||
|
|
||||||
|
The release flow is human-driven during the battle-test phase. Each step's output is visible; failures are immediately diagnosable. When the public-flip happens, steps 4, 5, 9, 10 move into a GitHub Actions `.github/workflows/release.yml` that triggers on tag push and runs the same scripts unmodified.
|
||||||
|
|
||||||
|
## 7. Homebrew tap
|
||||||
|
|
||||||
|
### 7.1 Tap repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
homebrew-tap/ (separate Gitea repo: git.sethpc.xyz/Seth/homebrew-tap)
|
||||||
|
├── README.md (instructions: how to tap, how to install)
|
||||||
|
└── Formula/
|
||||||
|
└── glabels-qt.rb (the formula)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Initial formula (starting point — finalized during plan execution)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class GlabelsQt < Formula
|
||||||
|
desc "gLabels Label Designer (Qt/C++) — Seth's packaging fork"
|
||||||
|
homepage "https://glabels.org"
|
||||||
|
url "https://git.sethpc.xyz/Seth/sethLabels.git",
|
||||||
|
tag: "3.99-master618-seth1",
|
||||||
|
revision: "<placeholder, filled at first release>"
|
||||||
|
license "GPL-3.0-only"
|
||||||
|
head "https://git.sethpc.xyz/Seth/sethLabels.git", branch: "main"
|
||||||
|
|
||||||
|
depends_on "cmake" => :build
|
||||||
|
depends_on "ninja" => :build
|
||||||
|
depends_on "pkgconf" => :build
|
||||||
|
depends_on "qt"
|
||||||
|
depends_on "zlib"
|
||||||
|
depends_on "qrencode" => :recommended # optional barcode backend
|
||||||
|
depends_on "zint" => :recommended # optional barcode backend
|
||||||
|
|
||||||
|
def install
|
||||||
|
system "cmake", "-S", ".", "-B", "build",
|
||||||
|
"-G", "Ninja",
|
||||||
|
"-DCMAKE_BUILD_TYPE=Release",
|
||||||
|
*std_cmake_args
|
||||||
|
system "cmake", "--build", "build"
|
||||||
|
system "cmake", "--install", "build"
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
assert_match "gLabels", shell_output("#{bin}/glabels-batch-qt --version")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 User install path (macOS)
|
||||||
|
|
||||||
|
```
|
||||||
|
brew tap seth/tap https://git.sethpc.xyz/Seth/homebrew-tap.git
|
||||||
|
brew install seth/tap/glabels-qt
|
||||||
|
```
|
||||||
|
|
||||||
|
The explicit URL form is required because brew defaults to GitHub for tap names. Documented in the tap's README. When the public-flip moves sethLabels (and the tap) to GitHub, the URL becomes the implicit GitHub default and the tap command shortens to `brew tap seth/tap`.
|
||||||
|
|
||||||
|
### 7.4 Tap maintenance per release
|
||||||
|
|
||||||
|
Single commit per release on the tap repo: bump `tag:` and `revision:` fields in `glabels-qt.rb`. No other edits expected.
|
||||||
|
|
||||||
|
## 8. Failure modes & guardrails
|
||||||
|
|
||||||
|
| # | Risk | Guardrail |
|
||||||
|
|---|------|-----------|
|
||||||
|
| F1 | Strict-zero policy creep | `check-no-upstream-edits.sh` runs at the start of each build script; allowlist explicit (see §5.5) |
|
||||||
|
| F2 | AppImage Qt plugin omissions (image formats, platform plugin) | linuxdeploy-plugin-qt covers most cases; smoke test (§5.3 step 6) launches each AppImage with `QT_QPA_PLATFORM=minimal` and verifies exit code |
|
||||||
|
| F3 | Build env drift (worked on steel141, fails on clean Debian 13 VM) | `deps-debian.sh` is the single source of truth for build deps; scripts fail fast on missing pkg with actionable apt-install command. Recommend re-validating on a fresh VM before each release. |
|
||||||
|
| F4 | Brew formula source URL changes (Gitea → GitHub on public flip) | Single-line edit in `glabels-qt.rb`; tap repo has its own commit history so the flip is one commit. Document the URL-flip in tap repo's README. |
|
||||||
|
| F5 | Re-package counter (`seth<N>`) collisions | Computed from existing tags via `git tag --list`, not from a state file; race-free under serial single-author releases. |
|
||||||
|
| F6 | Manual-build script rot | `scripts/README.md` documents required commands; future CI workflow YAML calls scripts directly — preventing recipe drift. Each release flow re-exercises the scripts. |
|
||||||
|
| F7 | Upstream rebase fails with conflicts (strict-zero broken without `check-no-upstream-edits.sh` having caught it) | Recover from a clean state in worst case: `git reset --hard upstream/master` then re-apply our scaffold commit by cherry-picking from `origin/main`. The single scaffold commit is the only thing to preserve. |
|
||||||
|
| F8 | dpkg-shlibdeps mis-detects a runtime dep, producing a `.deb` that won't install on a target system | Mitigation: smoke-test install on a clean Debian 13 VM as part of release flow (§6 step 11). If a false positive surfaces, override via `CPACK_DEBIAN_PACKAGE_DEPENDS` flag in `build-deb.sh`. |
|
||||||
|
| F9 | linuxdeploy or linuxdeploy-plugin-qt version drift breaks AppImage builds | `lib/linuxdeploy.sh` pins specific GitHub release versions; version bumps are deliberate, not transparent. |
|
||||||
|
|
||||||
|
## 9. Out of scope (explicit)
|
||||||
|
|
||||||
|
- **Windows packaging** — deferred per project brief. Upstream's NSIS support remains untouched and works for anyone who wants to build their own.
|
||||||
|
- **macOS `.dmg`, signed binaries, notarization** — replaced by Homebrew tap building from source.
|
||||||
|
- **Flatpak, Snap** — heavier infrastructure than warranted for the audience.
|
||||||
|
- **Arch AUR package** — already exists per upstream README (community-maintained `glabels-qt-git`).
|
||||||
|
- **Distribution to Debian official repos / backports / PPA / Ubuntu universe** — would require Debian Developer mentorship + ongoing policy compliance work; not the current goal.
|
||||||
|
- **Custom default templates baked into the package** — strict-zero forbids this. User-specific templates live in `~/.config/glabels-qt/templates/` outside the package, or in a future separate repo.
|
||||||
|
- **Branding, icon, splash, or string changes** — strict-zero forbids this.
|
||||||
|
- **Automated CI infrastructure (Gitea Actions runner, GitHub Actions, etc.) during the battle-test phase** — manual local builds. Reconsider at public-flip on GitHub.
|
||||||
|
- **GUI source changes of any kind** — strict-zero forbids this.
|
||||||
|
- **Repackaging upstream's existing tag-snapshot binaries** — we always build from source, never relabel upstream artifacts.
|
||||||
|
|
||||||
|
## 10. Smoke test definitions
|
||||||
|
|
||||||
|
The acceptance test for any release. All four must pass before a tag is pushed.
|
||||||
|
|
||||||
|
| # | Test | Pass condition |
|
||||||
|
|---|------|----------------|
|
||||||
|
| T1 | `dpkg-deb --info <deb>` parses and shows the correct version field | exit 0; version line matches `$VERSION` |
|
||||||
|
| T2 | `dpkg-deb --contents <deb>` includes both binaries | both `/usr/bin/glabels-qt` and `/usr/bin/glabels-batch-qt` present |
|
||||||
|
| T3 | `<batch-appimage> --version` runs | exit 0; output is non-empty |
|
||||||
|
| T4 | `QT_QPA_PLATFORM=minimal <gui-appimage> --help` runs | exit 0 |
|
||||||
|
| T5 (release-flow) | Install `.deb` on a clean Debian 13 VM, run `glabels-qt --version` | exit 0; version line matches `$VERSION` |
|
||||||
|
|
||||||
|
T5 is the strongest signal — it validates that `dpkg-shlibdeps` produced a correct depends list and that Qt6 from the system package manager satisfies runtime needs. Run T5 at least once per upstream-tag bump (i.e., on every `seth1` release; can be skipped on `seth2`+ if only packaging metadata changed).
|
||||||
|
|
||||||
|
## 11. Glossary
|
||||||
|
|
||||||
|
- **Deployment fork** — a fork whose only purpose is to ship binaries / install packages. No source-code intent. Diff against upstream is trivial; rebases are fast-forwards.
|
||||||
|
- **Strict zero (I1)** — the discipline that no upstream-tracked file is ever edited. All sethLabels content lives in NEW files in NEW top-level directories.
|
||||||
|
- **Battle-test phase** — the period before sethLabels is published as a formal public fork on GitHub. During this phase: manual builds, no CI, hosted at Gitea.
|
||||||
|
- **Public-flip** — the future event when sethLabels is moved (or dual-hosted) on GitHub as a public packaging fork. Triggers: GitHub Actions added as CI, brew tap source URL flips Gitea→GitHub, README updates.
|
||||||
|
- **Upstream-tag** — output of `git describe --tags --abbrev=0 upstream/master` against the j-evins/glabels-qt master branch. Currently of the form `3.99-master<N>` until upstream tags 4.0.
|
||||||
|
|
||||||
|
## 12. Open questions
|
||||||
|
|
||||||
|
None at spec time. All design questions raised during brainstorming were resolved before writing this document. Future open questions can be added here with their resolution moving to §3 once decided.
|
||||||
|
|
||||||
|
## 13. References
|
||||||
|
|
||||||
|
- [j-evins/glabels-qt README](https://github.com/j-evins/glabels-qt/blob/master/README.md) — the upstream "no binary releases" notice this fork addresses
|
||||||
|
- [`docs/BUILD-INSTRUCTIONS-LINUX.md`](../../docs/BUILD-INSTRUCTIONS-LINUX.md) — upstream's manual build instructions (the manual procedure our scripts automate)
|
||||||
|
- [CMake CPack DEB generator docs](https://cmake.org/cmake/help/latest/cpack_gen/deb.html)
|
||||||
|
- [linuxdeploy + qt plugin](https://github.com/linuxdeploy/linuxdeploy-plugin-qt)
|
||||||
|
- [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook)
|
||||||
|
- [Debian version comparison rules](https://www.debian.org/doc/debian-policy/ch-controlfields.html#version)
|
||||||
Reference in New Issue
Block a user