1816 lines
59 KiB
Markdown
1816 lines
59 KiB
Markdown
# sethLabels Packaging Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Implement the deployment-fork packaging pipeline defined by `sethlabels-docs/specs/2026-04-29-packaging-design.md` — produce installable `.deb` + AppImage artifacts on Debian-family Linux and a Homebrew tap formula for macOS, while honoring strict-zero-source-patches (Invariant I1).
|
||
|
||
**Architecture:** All sethLabels content lives in NEW top-level dirs (`scripts/`, `packaging/`, `sethlabels-docs/`, `tests-impl/`) and one new repo-root file (`README.sethlabels.md`). The Homebrew tap lives in a separate Gitea repo (`homebrew-tap`). Pure-logic scripts (`compute-version.sh`, `check-no-upstream-edits.sh`, `deps-debian.sh`) are TDD'd with `bats-core`. Build scripts (`build-deb.sh`, `build-appimages.sh`) are validated by inline smoke tests (T1–T4 from spec §10) against the artifacts they produce.
|
||
|
||
**Tech Stack:** Bash (with `set -euo pipefail`), CMake/CPack (already present upstream), `linuxdeploy` + `linuxdeploy-plugin-qt` (for AppImage bundling), `bats-core` (shell test framework, `apt install bats`), Homebrew Formula DSL (Ruby).
|
||
|
||
**Spec discrepancies to fix during implementation:**
|
||
|
||
1. **Spec §5.2 omits `-D CPACK_PACKAGE_NAME=glabels-qt`.** Upstream `CMakeLists.txt:86` sets `CPACK_PACKAGE_NAME=glabels` — without the override the `.deb` would be named `glabels_${VERSION}_amd64.deb`, contradicting decision D6. Task 7 adds the override.
|
||
2. **Spec §F9 requires pinned linuxdeploy versions, no specific tag named.** Task 5 has the implementer query GitHub for the latest release tag of `linuxdeploy/linuxdeploy` and `linuxdeploy/linuxdeploy-plugin-qt`, then hardcode those tags in `scripts/lib/linuxdeploy.sh`.
|
||
3. **Spec §2 calls `.gitignore` "a one-time scaffold-time touch", but §5.5's allowlist permits ongoing edits.** Task 1 adds new entries (`build/`, `scripts/.cache/`, `*.AppImage`); the guardrail allows this since `.gitignore` is in the allowlist.
|
||
|
||
**Plan-execution context:** This plan is best executed in a worktree via `superpowers:using-git-worktrees`. The plan does not create the worktree itself — the executor sets that up before starting Task 1.
|
||
|
||
---
|
||
|
||
### File Structure
|
||
|
||
| Path | Responsibility | Created in |
|
||
|------|----------------|------------|
|
||
| `scripts/compute-version.sh` | Emit `<upstream-tag>-seth<N>` to stdout. Pure logic. | Task 2 |
|
||
| `scripts/check-no-upstream-edits.sh` | Guardrail enforcing I1; exits 1 on any non-allowlisted upstream-file edit. | Task 3 |
|
||
| `scripts/lib/deps-debian.sh` | Single source of truth for build deps; sourceable + executable. | Task 4 |
|
||
| `scripts/lib/linuxdeploy.sh` | Bootstrap + cache `linuxdeploy` and `linuxdeploy-plugin-qt`. | Task 5 |
|
||
| `packaging/deb-metadata.env` | Maintainer + section + homepage for CPack DEB. | Task 6 |
|
||
| `packaging/appimage-recipe.env` | linuxdeploy plugin allowlist + exclude list. | Task 6 |
|
||
| `packaging/changelog.md` | Human-readable per-release notes. | Task 6 |
|
||
| `scripts/build-deb.sh` | Driver: deps → guardrail → version → cmake → cpack → smoke (T1, T2). | Task 7 |
|
||
| `scripts/build-appimages.sh` | Driver: deps → guardrail → version → cmake → linuxdeploy x2 → smoke (T3, T4). | Task 8 |
|
||
| `scripts/README.md` | Operator-facing run instructions. | Task 9 |
|
||
| `README.sethlabels.md` | Repo-root entry point: fork purpose, install methods, build path, link to spec. | Task 10 |
|
||
| `tests-impl/test-compute-version.bats` | Bats tests for `compute-version.sh`. | Task 2 |
|
||
| `tests-impl/test-check-no-upstream-edits.bats` | Bats tests for `check-no-upstream-edits.sh`. | Task 3 |
|
||
| `tests-impl/test-deps-debian.bats` | Bats tests for `deps-debian.sh`. | Task 4 |
|
||
| `tests-impl/run-all.sh` | One-shot runner: `bats tests-impl/*.bats`. | Task 1 |
|
||
| `~/bin/homebrew-tap/Formula/glabels-qt.rb` | Brew formula (separate repo). | Task 11 |
|
||
| `~/bin/homebrew-tap/README.md` | Tap install instructions. | Task 11 |
|
||
|
||
**Why `tests-impl/` and not `tests/`?** Upstream has `test-data/` at root but no `tests/`. Naming our test dir `tests-impl/` (impl = implementation tests, sethLabels-namespaced) avoids any visual collision and keeps the strict-zero boundary unmistakable.
|
||
|
||
---
|
||
|
||
## Task 1: Directory skeleton + .gitignore additions
|
||
|
||
**Files:**
|
||
- Modify: `.gitignore` (append three new patterns inside the existing sethLabels section)
|
||
- Create: `scripts/`, `scripts/lib/`, `packaging/`, `tests-impl/` (empty for now; populated by later tasks)
|
||
- Create: `tests-impl/run-all.sh`
|
||
|
||
- [ ] **Step 1: Verify clean working tree before starting**
|
||
|
||
```bash
|
||
git status
|
||
```
|
||
|
||
Expected: `nothing to commit, working tree clean` and the current branch is `main` (or your worktree branch). If dirty, stop and resolve before proceeding.
|
||
|
||
- [ ] **Step 2: Create the directory skeleton**
|
||
|
||
```bash
|
||
mkdir -p scripts/lib packaging tests-impl
|
||
```
|
||
|
||
- [ ] **Step 3: Append build-artifact patterns to `.gitignore`**
|
||
|
||
Open `.gitignore` and **append** to the existing `# === sethLabels (deployment fork) additions ===` section (do NOT modify upstream entries above it):
|
||
|
||
```
|
||
# Build outputs (out-of-tree)
|
||
build/
|
||
|
||
# linuxdeploy + plugin caches (downloaded by scripts/lib/linuxdeploy.sh on first AppImage build)
|
||
scripts/.cache/
|
||
|
||
# Locally-produced AppImage artifacts
|
||
*.AppImage
|
||
```
|
||
|
||
- [ ] **Step 4: Verify the strict-zero allowlist still covers .gitignore**
|
||
|
||
```bash
|
||
grep '\\.gitignore' sethlabels-docs/specs/2026-04-29-packaging-design.md
|
||
```
|
||
|
||
Expected: matches in §5.5 `allowed_pattern` and the §2 invariants table — `.gitignore` is allowlisted, so this edit does not violate I1.
|
||
|
||
- [ ] **Step 5: Create `tests-impl/run-all.sh`**
|
||
|
||
```bash
|
||
cat > tests-impl/run-all.sh <<'EOF'
|
||
#!/usr/bin/env bash
|
||
# Run the full bats test suite for sethLabels packaging scripts.
|
||
# Requires: bats (apt install bats).
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")"
|
||
exec bats *.bats
|
||
EOF
|
||
chmod +x tests-impl/run-all.sh
|
||
```
|
||
|
||
- [ ] **Step 6: Verify the new dirs are tracked-empty (or .keep'd)**
|
||
|
||
Empty dirs aren't tracked by git. We want the dirs visible after `git add`, so create a placeholder where needed and tracked-empty elsewhere is fine since later tasks populate them.
|
||
|
||
```bash
|
||
ls -la scripts/ scripts/lib/ packaging/ tests-impl/
|
||
```
|
||
|
||
Expected: each dir exists; `tests-impl/run-all.sh` is present and executable.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add .gitignore scripts/ packaging/ tests-impl/
|
||
git commit -m "chore: add packaging directory skeleton + .gitignore build patterns"
|
||
```
|
||
|
||
(`scripts/` and `packaging/` will commit only if non-empty — if not, that's fine; later tasks add files and push them.)
|
||
|
||
---
|
||
|
||
## Task 2: `scripts/compute-version.sh` (TDD)
|
||
|
||
**Files:**
|
||
- Test: `tests-impl/test-compute-version.bats`
|
||
- Create: `scripts/compute-version.sh`
|
||
|
||
**What it does:** Emits `<upstream-tag>-seth<N>` to stdout, where `<upstream-tag>` = `git describe --tags --abbrev=0 upstream/master` and `<N>` = count of existing `<upstream-tag>-seth*` tags + 1. Pure logic, idempotent under serial single-author releases (spec §5.4).
|
||
|
||
- [ ] **Step 1: Confirm `bats` is installed**
|
||
|
||
```bash
|
||
which bats || sudo apt install -y bats
|
||
bats --version
|
||
```
|
||
|
||
Expected: prints a version (>= 1.7).
|
||
|
||
- [ ] **Step 2: Write the failing tests**
|
||
|
||
Create `tests-impl/test-compute-version.bats`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bats
|
||
|
||
# Tests for scripts/compute-version.sh
|
||
# Invokes the real script against the real repo state.
|
||
|
||
setup() {
|
||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||
SCRIPT="$REPO_ROOT/scripts/compute-version.sh"
|
||
}
|
||
|
||
@test "script exists and is executable" {
|
||
[ -x "$SCRIPT" ]
|
||
}
|
||
|
||
@test "output matches '<upstream-tag>-seth<N>' format" {
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 0 ]
|
||
[[ "$output" =~ ^[0-9].+-seth[0-9]+$ ]]
|
||
}
|
||
|
||
@test "N=1 when no prior seth-tags exist" {
|
||
# This test assumes a clean tag db. If there ARE existing seth-tags, this
|
||
# test will report N>1 and that's the correct value. Skipped if any
|
||
# upstream-tag-seth* tag already exists.
|
||
upstream_tag=$(git describe --tags --abbrev=0 upstream/master)
|
||
existing=$(git tag --list "${upstream_tag}-seth*" | wc -l)
|
||
if [ "$existing" -gt 0 ]; then
|
||
skip "seth-tags already exist (count=$existing); N=1 invariant only holds on first release"
|
||
fi
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 0 ]
|
||
[[ "$output" == "${upstream_tag}-seth1" ]]
|
||
}
|
||
|
||
@test "N increments past existing seth-tags" {
|
||
upstream_tag=$(git describe --tags --abbrev=0 upstream/master)
|
||
# Create a fake seth-tag for this test, then clean up.
|
||
fake_tag="${upstream_tag}-seth99"
|
||
git tag "$fake_tag" 2>/dev/null || true
|
||
run "$SCRIPT"
|
||
git tag -d "$fake_tag" >/dev/null 2>&1 || true
|
||
[ "$status" -eq 0 ]
|
||
# Existing count was at least 1 (our fake), so N >= 2.
|
||
n="${output##*-seth}"
|
||
[ "$n" -ge 2 ]
|
||
}
|
||
|
||
@test "fails cleanly when upstream/master ref is missing" {
|
||
# Run in a temp git repo with no upstream remote.
|
||
tmp=$(mktemp -d)
|
||
cd "$tmp"
|
||
git init -q
|
||
git commit --allow-empty -m "init" -q
|
||
run "$SCRIPT"
|
||
cd "$REPO_ROOT"
|
||
rm -rf "$tmp"
|
||
[ "$status" -ne 0 ]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Run tests; confirm they fail**
|
||
|
||
```bash
|
||
bats tests-impl/test-compute-version.bats
|
||
```
|
||
|
||
Expected: all tests fail because `scripts/compute-version.sh` doesn't exist yet. The first test (`script exists and is executable`) reports `[ -x "$SCRIPT" ]` failed.
|
||
|
||
- [ ] **Step 4: Write the minimal script to pass the tests**
|
||
|
||
Create `scripts/compute-version.sh`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Emit "<upstream-tag>-seth<N>" version string to stdout.
|
||
# Pure logic: no side effects.
|
||
#
|
||
# CALLER RESPONSIBILITY (per spec §5.4): the local tag db must be fresh.
|
||
# If invoked outside the release flow, run `git fetch origin --tags` first
|
||
# or risk a stale <N> value.
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.4 (D4)
|
||
set -euo pipefail
|
||
|
||
upstream_tag=$(git describe --tags --abbrev=0 upstream/master)
|
||
existing_count=$(git tag --list "${upstream_tag}-seth*" | wc -l | tr -d ' ')
|
||
next_n=$((existing_count + 1))
|
||
echo "${upstream_tag}-seth${next_n}"
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/compute-version.sh
|
||
```
|
||
|
||
- [ ] **Step 5: Run tests; confirm they pass**
|
||
|
||
```bash
|
||
bats tests-impl/test-compute-version.bats
|
||
```
|
||
|
||
Expected: all tests pass (or the `N=1` test reports `skip` if you've already tagged a release in your worktree, which is fine).
|
||
|
||
- [ ] **Step 6: Sanity-check the actual output**
|
||
|
||
```bash
|
||
./scripts/compute-version.sh
|
||
```
|
||
|
||
Expected: `3.99-master618-seth1` (or higher `seth<N>` if you've tagged before). The `3.99-master618` part should match `git describe --tags --abbrev=0 upstream/master`.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add scripts/compute-version.sh tests-impl/test-compute-version.bats
|
||
git commit -m "feat: add compute-version.sh + bats tests"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: `scripts/check-no-upstream-edits.sh` (TDD)
|
||
|
||
**Files:**
|
||
- Test: `tests-impl/test-check-no-upstream-edits.bats`
|
||
- Create: `scripts/check-no-upstream-edits.sh`
|
||
|
||
**What it does:** Enforces Invariant I1. Exits 0 silently on clean state; exits 1 with a clear error listing violations otherwise. Catches BOTH committed and uncommitted edits (spec §5.5).
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
Create `tests-impl/test-check-no-upstream-edits.bats`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bats
|
||
|
||
# Tests for scripts/check-no-upstream-edits.sh
|
||
|
||
setup() {
|
||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||
SCRIPT="$REPO_ROOT/scripts/check-no-upstream-edits.sh"
|
||
TMP_REPO=""
|
||
}
|
||
|
||
teardown() {
|
||
if [ -n "$TMP_REPO" ] && [ -d "$TMP_REPO" ]; then
|
||
rm -rf "$TMP_REPO"
|
||
fi
|
||
}
|
||
|
||
# --- Helpers ---
|
||
|
||
# Build a minimal disposable repo that mimics the sethLabels structure with a
|
||
# local "upstream/master" ref. Returns its path via stdout.
|
||
make_test_repo() {
|
||
local tmp=$(mktemp -d)
|
||
cd "$tmp"
|
||
git init -q -b master
|
||
git config user.email "test@test"
|
||
git config user.name "test"
|
||
# Pretend-upstream files
|
||
echo "upstream content" > UPSTREAM_FILE.md
|
||
echo "real source" > glabels-source.cpp
|
||
git add UPSTREAM_FILE.md glabels-source.cpp
|
||
git commit -m "upstream base" -q
|
||
# Create a local "upstream/master" ref pointing here
|
||
git update-ref refs/remotes/upstream/master HEAD
|
||
# Create a feature branch for sethLabels content
|
||
git checkout -q -b main
|
||
echo "$tmp"
|
||
}
|
||
|
||
# --- Tests ---
|
||
|
||
@test "script exists and is executable" {
|
||
[ -x "$SCRIPT" ]
|
||
}
|
||
|
||
@test "exits 0 on clean state with only allowlisted committed changes" {
|
||
TMP_REPO=$(make_test_repo)
|
||
cd "$TMP_REPO"
|
||
mkdir -p scripts packaging sethlabels-docs .claude/handoffs
|
||
echo "test" > CLAUDE.md
|
||
echo "test" > scripts/something.sh
|
||
echo "test" > packaging/x.env
|
||
echo "test" > sethlabels-docs/spec.md
|
||
echo "" >> .gitignore
|
||
git add -A
|
||
git commit -m "sethLabels additions" -q
|
||
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 0 ]
|
||
[ -z "$output" ]
|
||
}
|
||
|
||
@test "exits 1 when an upstream file is committed-modified" {
|
||
TMP_REPO=$(make_test_repo)
|
||
cd "$TMP_REPO"
|
||
echo "evil edit" >> glabels-source.cpp
|
||
git add glabels-source.cpp
|
||
git commit -m "BAD: edit upstream file" -q
|
||
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 1 ]
|
||
[[ "$output" == *"glabels-source.cpp"* ]]
|
||
[[ "$output" == *"strict-zero"* ]]
|
||
}
|
||
|
||
@test "exits 1 when an upstream file has uncommitted working-tree edits" {
|
||
TMP_REPO=$(make_test_repo)
|
||
cd "$TMP_REPO"
|
||
echo "uncommitted evil edit" >> glabels-source.cpp
|
||
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 1 ]
|
||
[[ "$output" == *"glabels-source.cpp"* ]]
|
||
}
|
||
|
||
@test "exits 0 when only .gitignore is modified (allowlisted)" {
|
||
TMP_REPO=$(make_test_repo)
|
||
cd "$TMP_REPO"
|
||
echo "*.tmp" >> .gitignore
|
||
git add .gitignore
|
||
git commit -m "extend gitignore" -q
|
||
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 0 ]
|
||
}
|
||
|
||
@test "exits 0 when only CLAUDE.md / IDEA.md / DECISIONS.md / README.sethlabels.md are added" {
|
||
TMP_REPO=$(make_test_repo)
|
||
cd "$TMP_REPO"
|
||
echo "x" > CLAUDE.md
|
||
echo "x" > IDEA.md
|
||
echo "x" > DECISIONS.md
|
||
echo "x" > README.sethlabels.md
|
||
git add -A
|
||
git commit -m "add sethLabels root docs" -q
|
||
|
||
run "$SCRIPT"
|
||
[ "$status" -eq 0 ]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests; confirm they fail**
|
||
|
||
```bash
|
||
bats tests-impl/test-check-no-upstream-edits.bats
|
||
```
|
||
|
||
Expected: all tests fail (script doesn't exist).
|
||
|
||
- [ ] **Step 3: Write the script to pass**
|
||
|
||
Create `scripts/check-no-upstream-edits.sh`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Enforce Invariant I1: no upstream-tracked file is ever edited.
|
||
# Exits 0 on clean state, 1 on violation.
|
||
#
|
||
# Catches BOTH committed drift (commits unique to HEAD vs upstream/master)
|
||
# AND working-tree drift (uncommitted local edits to tracked files).
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.5 (I1, F1)
|
||
set -euo pipefail
|
||
|
||
# Allowlist: files/dirs sethLabels is permitted to add or modify.
|
||
# `.gitignore` is the one upstream-file exception (called out in spec §2).
|
||
allowed_pattern='^\.gitignore$|^\.claude/|^scripts/|^packaging/|^sethlabels-docs/|^tests-impl/|^README\.sethlabels\.md$|^CLAUDE\.md$|^IDEA\.md$|^DECISIONS\.md$'
|
||
|
||
committed=$(git diff --name-only upstream/master..HEAD 2>/dev/null || true)
|
||
working=$(git diff --name-only HEAD 2>/dev/null || true)
|
||
|
||
all_changes=$(printf "%s\n%s\n" "$committed" "$working" | sort -u | sed '/^$/d')
|
||
|
||
if [ -z "$all_changes" ]; then
|
||
exit 0
|
||
fi
|
||
|
||
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 ""
|
||
echo "See sethlabels-docs/specs/2026-04-29-packaging-design.md §I1."
|
||
exit 1
|
||
fi
|
||
|
||
exit 0
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/check-no-upstream-edits.sh
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests; confirm they pass**
|
||
|
||
```bash
|
||
bats tests-impl/test-check-no-upstream-edits.bats
|
||
```
|
||
|
||
Expected: all 6 tests pass.
|
||
|
||
- [ ] **Step 5: Run the guardrail against the real repo**
|
||
|
||
```bash
|
||
./scripts/check-no-upstream-edits.sh && echo "CLEAN"
|
||
```
|
||
|
||
Expected: prints `CLEAN`. If it errors, you've accidentally touched an upstream file — investigate before continuing.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add scripts/check-no-upstream-edits.sh tests-impl/test-check-no-upstream-edits.bats
|
||
git commit -m "feat: add check-no-upstream-edits.sh + bats tests (enforces I1)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: `scripts/lib/deps-debian.sh` (TDD)
|
||
|
||
**Files:**
|
||
- Test: `tests-impl/test-deps-debian.bats`
|
||
- Create: `scripts/lib/deps-debian.sh`
|
||
|
||
**What it does:** Single source of truth for build deps (spec §5.1). When sourced, exposes `SETHLABELS_DEPS` array. When executed, checks each dep is installed and prints an actionable `apt install ...` command if anything is missing.
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
Create `tests-impl/test-deps-debian.bats`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bats
|
||
|
||
# Tests for scripts/lib/deps-debian.sh
|
||
|
||
setup() {
|
||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||
SCRIPT="$REPO_ROOT/scripts/lib/deps-debian.sh"
|
||
}
|
||
|
||
@test "script exists and is executable" {
|
||
[ -x "$SCRIPT" ]
|
||
}
|
||
|
||
@test "sourceable: exposes SETHLABELS_DEPS array" {
|
||
source "$SCRIPT"
|
||
[ "${#SETHLABELS_DEPS[@]}" -gt 5 ]
|
||
}
|
||
|
||
@test "SETHLABELS_DEPS contains core build tools" {
|
||
source "$SCRIPT"
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" cmake "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" ninja-build "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" build-essential "* ]]
|
||
}
|
||
|
||
@test "SETHLABELS_DEPS contains Qt6 libraries" {
|
||
source "$SCRIPT"
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" qt6-base-dev "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" qt6-svg-dev "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" qt6-tools-dev "* ]]
|
||
}
|
||
|
||
@test "SETHLABELS_DEPS contains barcode + zlib deps" {
|
||
source "$SCRIPT"
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" zlib1g-dev "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" libqrencode-dev "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" libzint-dev "* ]]
|
||
}
|
||
|
||
@test "SETHLABELS_DEPS contains packaging tools" {
|
||
source "$SCRIPT"
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" dpkg-dev "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" fakeroot "* ]]
|
||
[[ " ${SETHLABELS_DEPS[*]} " == *" wget "* ]]
|
||
}
|
||
|
||
@test "executed: prints status and exits 0 (when all installed) OR 1 with apt-install hint" {
|
||
run "$SCRIPT"
|
||
if [ "$status" -eq 0 ]; then
|
||
[[ "$output" == *"All build dependencies present"* ]]
|
||
else
|
||
[[ "$output" == *"sudo apt install"* ]]
|
||
fi
|
||
}
|
||
|
||
@test "executed: warns if not on Debian/Ubuntu" {
|
||
# Simulate non-Debian by overriding /etc/os-release path via env var
|
||
if [ -f /etc/os-release ] && grep -qE '^ID=(debian|ubuntu)' /etc/os-release; then
|
||
skip "currently on Debian/Ubuntu — non-Debian path covered by source review"
|
||
fi
|
||
run "$SCRIPT"
|
||
[[ "$output" == *"Debian"* || "$output" == *"Ubuntu"* ]]
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests; confirm they fail**
|
||
|
||
```bash
|
||
bats tests-impl/test-deps-debian.bats
|
||
```
|
||
|
||
Expected: all fail.
|
||
|
||
- [ ] **Step 3: Write the script to pass**
|
||
|
||
Create `scripts/lib/deps-debian.sh`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Single source of truth for sethLabels build dependencies on Debian-family Linux.
|
||
#
|
||
# When SOURCED: exposes SETHLABELS_DEPS array (no side effects).
|
||
# When EXECUTED: verifies each dep is installed; prints actionable
|
||
# `sudo apt install ...` command on missing deps; exits 1.
|
||
# On clean state, prints "All build dependencies present." and exits 0.
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.1
|
||
set -euo pipefail
|
||
|
||
SETHLABELS_DEPS=(
|
||
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
|
||
bats
|
||
)
|
||
|
||
# Detect sourced vs. executed.
|
||
# When sourced: BASH_SOURCE[0] != $0
|
||
# When executed: BASH_SOURCE[0] == $0
|
||
if [ "${BASH_SOURCE[0]}" != "${0}" ]; then
|
||
return 0 2>/dev/null || exit 0
|
||
fi
|
||
|
||
# --- Executed path ---
|
||
|
||
# Sanity check the build host
|
||
if [ ! -f /etc/os-release ]; then
|
||
echo "WARNING: /etc/os-release missing; not Debian-family. This script is designed for Debian 13 / Ubuntu LTS." >&2
|
||
fi
|
||
|
||
if [ -f /etc/os-release ]; then
|
||
. /etc/os-release
|
||
if [[ "${ID:-}" != "debian" && "${ID:-}" != "ubuntu" ]] && [[ "${ID_LIKE:-}" != *debian* && "${ID_LIKE:-}" != *ubuntu* ]]; then
|
||
echo "WARNING: not running on Debian or Ubuntu (detected ID='${ID:-unknown}'). Build deps may differ." >&2
|
||
fi
|
||
fi
|
||
|
||
missing=()
|
||
for pkg in "${SETHLABELS_DEPS[@]}"; do
|
||
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
|
||
missing+=("$pkg")
|
||
fi
|
||
done
|
||
|
||
if [ "${#missing[@]}" -gt 0 ]; then
|
||
echo "Missing build dependencies (${#missing[@]}):"
|
||
for p in "${missing[@]}"; do
|
||
echo " - $p"
|
||
done
|
||
echo ""
|
||
echo "Install with:"
|
||
echo " sudo apt install -y ${missing[*]}"
|
||
exit 1
|
||
fi
|
||
|
||
echo "All build dependencies present (${#SETHLABELS_DEPS[@]} packages verified)."
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/lib/deps-debian.sh
|
||
```
|
||
|
||
- [ ] **Step 4: Run tests; confirm they pass**
|
||
|
||
```bash
|
||
bats tests-impl/test-deps-debian.bats
|
||
```
|
||
|
||
Expected: all 8 tests pass.
|
||
|
||
- [ ] **Step 5: Run the script directly**
|
||
|
||
```bash
|
||
./scripts/lib/deps-debian.sh
|
||
```
|
||
|
||
Expected: either `All build dependencies present.` (if you've installed everything) OR a `Missing build dependencies` listing followed by an `apt install` command. If missing, copy the printed command and run it now — it's needed for Tasks 7 and 8.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add scripts/lib/deps-debian.sh tests-impl/test-deps-debian.bats
|
||
git commit -m "feat: add deps-debian.sh (build-dep manifest + checker)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: `scripts/lib/linuxdeploy.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/lib/linuxdeploy.sh`
|
||
|
||
**What it does:** Bootstraps `linuxdeploy` and `linuxdeploy-plugin-qt` to a script-local cache (`scripts/.cache/`) on first run. Pinned versions per spec §F9 (no rolling `continuous` tag).
|
||
|
||
This task does NOT use TDD because it makes network calls. We validate it by running it and checking outputs.
|
||
|
||
- [ ] **Step 1: Discover the latest pinned tags from GitHub**
|
||
|
||
Per spec §F9, we must pin specific versions. Query GitHub for the latest releases:
|
||
|
||
```bash
|
||
curl -s https://api.github.com/repos/linuxdeploy/linuxdeploy/releases/latest | grep -E '"tag_name"' | head -1
|
||
curl -s https://api.github.com/repos/linuxdeploy/linuxdeploy-plugin-qt/releases/latest | grep -E '"tag_name"' | head -1
|
||
```
|
||
|
||
Record both tag values. As of spec time (2026-04-29) the linuxdeploy project uses rolling `continuous` releases plus dated `1-alpha-*` snapshots; pick the most recent dated `1-alpha-YYYYMMDD-N` tag from the releases page (NOT `continuous` — `continuous` violates F9). If only `continuous` is offered for the qt plugin, fall back to its `master`-pinned commit SHA noted in the response.
|
||
|
||
For the rest of this task, substitute your discovered tags as `LINUXDEPLOY_TAG` and `LINUXDEPLOY_PLUGIN_QT_TAG` in the script below.
|
||
|
||
- [ ] **Step 2: Write the script**
|
||
|
||
Create `scripts/lib/linuxdeploy.sh` (replace the two `<TAG>` placeholders below with the actual tags discovered in Step 1):
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Bootstrap linuxdeploy + linuxdeploy-plugin-qt to a script-local cache.
|
||
#
|
||
# When SOURCED: exposes $LINUXDEPLOY_BIN and $LINUXDEPLOY_PLUGIN_QT_BIN paths
|
||
# (downloads on first run if missing).
|
||
# When EXECUTED: ensures both binaries are present and prints their paths.
|
||
#
|
||
# Pinned per spec §F9 — version bumps are deliberate, not transparent.
|
||
# To bump: re-run discovery (Task 5 Step 1 of the implementation plan), update
|
||
# the two TAG variables below, and verify a fresh AppImage build.
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §F9
|
||
set -euo pipefail
|
||
|
||
# === PINNED VERSIONS (update deliberately per F9) ===
|
||
LINUXDEPLOY_TAG="<REPLACE_WITH_DISCOVERED_TAG>"
|
||
LINUXDEPLOY_PLUGIN_QT_TAG="<REPLACE_WITH_DISCOVERED_TAG>"
|
||
# ====================================================
|
||
|
||
CACHE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/.cache"
|
||
LINUXDEPLOY_BIN="$CACHE_DIR/linuxdeploy-${LINUXDEPLOY_TAG}-x86_64.AppImage"
|
||
LINUXDEPLOY_PLUGIN_QT_BIN="$CACHE_DIR/linuxdeploy-plugin-qt-${LINUXDEPLOY_PLUGIN_QT_TAG}-x86_64.AppImage"
|
||
|
||
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/${LINUXDEPLOY_TAG}/linuxdeploy-x86_64.AppImage"
|
||
LINUXDEPLOY_PLUGIN_QT_URL="https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/${LINUXDEPLOY_PLUGIN_QT_TAG}/linuxdeploy-plugin-qt-x86_64.AppImage"
|
||
|
||
ensure_tool() {
|
||
local url="$1" out="$2" label="$3"
|
||
if [ -x "$out" ]; then
|
||
return 0
|
||
fi
|
||
mkdir -p "$(dirname "$out")"
|
||
echo "Downloading $label from $url ..." >&2
|
||
if ! wget -q --show-progress -O "$out" "$url"; then
|
||
echo "ERROR: download failed for $label ($url)" >&2
|
||
rm -f "$out"
|
||
return 1
|
||
fi
|
||
chmod +x "$out"
|
||
}
|
||
|
||
ensure_tool "$LINUXDEPLOY_URL" "$LINUXDEPLOY_BIN" "linuxdeploy"
|
||
ensure_tool "$LINUXDEPLOY_PLUGIN_QT_URL" "$LINUXDEPLOY_PLUGIN_QT_BIN" "linuxdeploy-plugin-qt"
|
||
|
||
export LINUXDEPLOY_BIN LINUXDEPLOY_PLUGIN_QT_BIN
|
||
|
||
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
||
echo "linuxdeploy: $LINUXDEPLOY_BIN"
|
||
echo "linuxdeploy-plugin-qt: $LINUXDEPLOY_PLUGIN_QT_BIN"
|
||
fi
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/lib/linuxdeploy.sh
|
||
```
|
||
|
||
- [ ] **Step 3: Verify the cache dir is gitignored**
|
||
|
||
```bash
|
||
git check-ignore -v scripts/.cache/anything 2>&1 || echo "NOT IGNORED (problem)"
|
||
```
|
||
|
||
Expected: shows that `scripts/.cache/` matches a `.gitignore` rule (added in Task 1 Step 3). If it says "NOT IGNORED", revisit Task 1 Step 3.
|
||
|
||
- [ ] **Step 4: Run the script (downloads ~30MB on first run)**
|
||
|
||
```bash
|
||
./scripts/lib/linuxdeploy.sh
|
||
```
|
||
|
||
Expected: prints two paths under `scripts/.cache/`. Both files must be executable AppImages.
|
||
|
||
- [ ] **Step 5: Smoke-verify the downloaded tools work**
|
||
|
||
```bash
|
||
"$(./scripts/lib/linuxdeploy.sh | head -1 | awk '{print $2}')" --version
|
||
```
|
||
|
||
Expected: prints linuxdeploy version banner. If FUSE is not available (some VMs / containers), set `APPIMAGE_EXTRACT_AND_RUN=1`:
|
||
|
||
```bash
|
||
APPIMAGE_EXTRACT_AND_RUN=1 "$LINUXDEPLOY_BIN" --version
|
||
```
|
||
|
||
If this fails, ensure `libfuse2` is installed (`sudo apt install libfuse2`) or use the extract-and-run env var.
|
||
|
||
- [ ] **Step 6: Re-run to verify caching (no re-download)**
|
||
|
||
```bash
|
||
./scripts/lib/linuxdeploy.sh
|
||
```
|
||
|
||
Expected: instant exit, no `Downloading ...` messages.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add scripts/lib/linuxdeploy.sh
|
||
git commit -m "feat: add linuxdeploy.sh bootstrap (pinned per F9)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Packaging metadata files
|
||
|
||
**Files:**
|
||
- Create: `packaging/deb-metadata.env`
|
||
- Create: `packaging/appimage-recipe.env`
|
||
- Create: `packaging/changelog.md`
|
||
|
||
**What they do:** Static configuration files consumed by the build scripts. Keeping these out of the shell scripts means tweaking maintainer info, brew tap pins, or release notes is a single-line edit, not a script change.
|
||
|
||
- [ ] **Step 1: Create `packaging/deb-metadata.env`**
|
||
|
||
```bash
|
||
cat > packaging/deb-metadata.env <<'EOF'
|
||
# CPack DEB metadata overrides — sourced by scripts/build-deb.sh.
|
||
# All values are passed to cpack as -D CPACK_DEBIAN_PACKAGE_<KEY>="$VALUE".
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.2
|
||
|
||
MAINTAINER="Seth Freiberg <seth@sethfreiberg.com>"
|
||
SECTION="graphics"
|
||
HOMEPAGE="https://glabels.org"
|
||
# CPACK_PACKAGE_NAME override — required because upstream sets
|
||
# CPACK_PACKAGE_NAME=glabels (CMakeLists.txt:86) and decision D6 wants glabels-qt.
|
||
PACKAGE_NAME="glabels-qt"
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 2: Create `packaging/appimage-recipe.env`**
|
||
|
||
```bash
|
||
cat > packaging/appimage-recipe.env <<'EOF'
|
||
# linuxdeploy / linuxdeploy-plugin-qt configuration — sourced by scripts/build-appimages.sh.
|
||
# Documents the bundling choices for both AppImages.
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.3
|
||
|
||
# Qt platform plugins to include (linuxdeploy-plugin-qt picks these up automatically;
|
||
# documenting here for posterity).
|
||
QT_PLATFORM_PLUGINS="xcb"
|
||
|
||
# Image format plugins. The GUI app needs SVG/PNG support; batch CLI does not.
|
||
QT_IMAGE_FORMAT_PLUGINS_GUI="svg"
|
||
QT_IMAGE_FORMAT_PLUGINS_BATCH=""
|
||
|
||
# Files in the AppDir we don't want bundled (linuxdeploy is greedy by default).
|
||
APPDIR_EXCLUDE_GLOBS=()
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 3: Create `packaging/changelog.md`**
|
||
|
||
```bash
|
||
cat > packaging/changelog.md <<'EOF'
|
||
# sethLabels packaging changelog
|
||
|
||
Per-release packaging notes. Each entry covers what changed in the *packaging*,
|
||
not what changed upstream. For application-level changes, see the upstream
|
||
`docs/CHANGELOG.md` and `git log upstream/master`.
|
||
|
||
## Format
|
||
|
||
```
|
||
## <version> — <ISO date>
|
||
|
||
- bullet describing what changed in this packaging release
|
||
- ...
|
||
```
|
||
|
||
`<version>` is `<upstream-tag>-seth<N>` (e.g., `3.99-master618-seth1`), matching
|
||
the git tag and the artifact filename. See spec §D4.
|
||
|
||
---
|
||
|
||
## (unreleased)
|
||
|
||
- First end-to-end release dry run pending.
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 4: Smoke-source the env files**
|
||
|
||
```bash
|
||
( source packaging/deb-metadata.env && echo "MAINTAINER=$MAINTAINER" && echo "PACKAGE_NAME=$PACKAGE_NAME" )
|
||
( source packaging/appimage-recipe.env && echo "QT_PLATFORM_PLUGINS=$QT_PLATFORM_PLUGINS" )
|
||
```
|
||
|
||
Expected: prints the values, no errors. If `set -u` is on globally, sourcing should not error (no unset vars referenced).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add packaging/
|
||
git commit -m "feat: add packaging metadata + initial changelog"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: `scripts/build-deb.sh` (with inline smoke tests T1, T2)
|
||
|
||
**Files:**
|
||
- Create: `scripts/build-deb.sh`
|
||
|
||
**What it does:** End-to-end driver that produces `build/deb/glabels-qt_${VERSION}_amd64.deb` and runs smoke tests T1 (parse-ability) and T2 (binaries present) inline. Aborts with a clear error on any failure.
|
||
|
||
- [ ] **Step 1: Write the script**
|
||
|
||
Create `scripts/build-deb.sh`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Build the sethLabels .deb package.
|
||
#
|
||
# Pipeline (spec §5.2):
|
||
# 1. sanity check build host (Debian/Ubuntu, deps present)
|
||
# 2. strict-zero guardrail
|
||
# 3. compute version
|
||
# 4. out-of-tree cmake build
|
||
# 5. CPack with overrides
|
||
# 6. inline smoke tests T1, T2
|
||
# 7. print artifact path for the operator
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.2
|
||
set -euo pipefail
|
||
|
||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$REPO_ROOT"
|
||
|
||
echo "==> [1/6] Sanity check build host"
|
||
"$REPO_ROOT/scripts/lib/deps-debian.sh"
|
||
|
||
echo "==> [2/6] Strict-zero guardrail"
|
||
"$REPO_ROOT/scripts/check-no-upstream-edits.sh"
|
||
|
||
echo "==> [3/6] Compute version"
|
||
VERSION="$("$REPO_ROOT/scripts/compute-version.sh")"
|
||
echo " VERSION = $VERSION"
|
||
|
||
echo "==> [4/6] Out-of-tree cmake build"
|
||
BUILD_DIR="$REPO_ROOT/build/deb"
|
||
rm -rf "$BUILD_DIR"
|
||
mkdir -p "$BUILD_DIR"
|
||
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||
cmake --build "$BUILD_DIR" --parallel
|
||
|
||
echo "==> [5/6] CPack DEB generation"
|
||
# shellcheck disable=SC1091
|
||
source "$REPO_ROOT/packaging/deb-metadata.env"
|
||
|
||
cd "$BUILD_DIR"
|
||
cpack -G DEB \
|
||
-D CPACK_PACKAGE_NAME="$PACKAGE_NAME" \
|
||
-D CPACK_PACKAGE_VERSION="$VERSION" \
|
||
-D CPACK_DEBIAN_PACKAGE_NAME="$PACKAGE_NAME" \
|
||
-D CPACK_DEBIAN_PACKAGE_MAINTAINER="$MAINTAINER" \
|
||
-D CPACK_DEBIAN_PACKAGE_SECTION="$SECTION" \
|
||
-D CPACK_DEBIAN_PACKAGE_HOMEPAGE="$HOMEPAGE" \
|
||
-D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON \
|
||
-D CPACK_DEBIAN_FILE_NAME=DEB-DEFAULT
|
||
cd "$REPO_ROOT"
|
||
|
||
# Resolve the actual artifact filename (CPack uses DEB-DEFAULT naming convention).
|
||
DEB_ARTIFACT=$(ls "$BUILD_DIR"/${PACKAGE_NAME}_*.deb 2>/dev/null | head -1)
|
||
if [ -z "$DEB_ARTIFACT" ] || [ ! -f "$DEB_ARTIFACT" ]; then
|
||
echo "ERROR: expected .deb artifact not found in $BUILD_DIR" >&2
|
||
ls -la "$BUILD_DIR" >&2
|
||
exit 1
|
||
fi
|
||
|
||
echo "==> [6/6] Smoke tests"
|
||
|
||
# T1: dpkg-deb --info parses, version field matches.
|
||
echo " T1: dpkg-deb --info"
|
||
T1_OUT=$(dpkg-deb --info "$DEB_ARTIFACT")
|
||
if ! echo "$T1_OUT" | grep -qE "^ Version: ${VERSION}$"; then
|
||
echo "ERROR: T1 failed — version field in .deb does not match \$VERSION=$VERSION" >&2
|
||
echo "$T1_OUT" >&2
|
||
exit 1
|
||
fi
|
||
echo " T1: PASS"
|
||
|
||
# T2: dpkg-deb --contents includes both binaries.
|
||
echo " T2: dpkg-deb --contents"
|
||
T2_OUT=$(dpkg-deb --contents "$DEB_ARTIFACT")
|
||
if ! echo "$T2_OUT" | grep -q '/usr/bin/glabels-qt'; then
|
||
echo "ERROR: T2 failed — /usr/bin/glabels-qt missing from .deb" >&2
|
||
exit 1
|
||
fi
|
||
if ! echo "$T2_OUT" | grep -q '/usr/bin/glabels-batch-qt'; then
|
||
echo "ERROR: T2 failed — /usr/bin/glabels-batch-qt missing from .deb" >&2
|
||
exit 1
|
||
fi
|
||
echo " T2: PASS"
|
||
|
||
# Optional: lintian (warnings-only, non-fatal during battle-test).
|
||
if command -v lintian >/dev/null 2>&1; then
|
||
echo " lintian (advisory):"
|
||
lintian "$DEB_ARTIFACT" || true
|
||
fi
|
||
|
||
echo ""
|
||
echo "Artifact: $DEB_ARTIFACT"
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/build-deb.sh
|
||
```
|
||
|
||
- [ ] **Step 2: Run the build**
|
||
|
||
```bash
|
||
./scripts/build-deb.sh
|
||
```
|
||
|
||
Expected: walks through all 6 steps, ends with `Artifact: build/deb/glabels-qt_3.99-master618-seth1_amd64.deb`. Wall time ~2 minutes on a modern machine.
|
||
|
||
If T1 fails: investigate whether `CPACK_PACKAGE_VERSION` was applied correctly; some upstream `CMakeLists.txt` edits may need a clean build.
|
||
|
||
If T2 fails: check `cmake --install --prefix=/tmp/install build/deb && ls /tmp/install/usr/bin/` to verify upstream's install rules produced both binaries. If only one is present, it's an upstream issue (not a sethLabels bug).
|
||
|
||
- [ ] **Step 3: Inspect the artifact manually**
|
||
|
||
```bash
|
||
DEB=$(ls build/deb/glabels-qt_*.deb | head -1)
|
||
dpkg-deb --info "$DEB"
|
||
dpkg-deb --contents "$DEB" | head -30
|
||
```
|
||
|
||
Expected: `Package: glabels-qt`, `Version: 3.99-master618-seth1` (or your computed version), and a sane file listing showing `/usr/bin/...`, `/usr/share/applications/...`, etc.
|
||
|
||
- [ ] **Step 4: Optionally test-install on the build host (non-destructive smoke)**
|
||
|
||
```bash
|
||
sudo apt install -y "./$DEB"
|
||
glabels-qt --version
|
||
sudo apt remove -y glabels-qt
|
||
```
|
||
|
||
Skip if you'd rather only test on a clean VM (T5 in the release flow).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/build-deb.sh
|
||
git commit -m "feat: add build-deb.sh with inline smoke tests T1, T2"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: `scripts/build-appimages.sh` (with inline smoke tests T3, T4)
|
||
|
||
**Files:**
|
||
- Create: `scripts/build-appimages.sh`
|
||
|
||
**What it does:** Produces TWO AppImages — `sethlabels-gui-${VERSION}-x86_64.AppImage` (full Qt6 GUI) and `sethlabels-batch-${VERSION}-x86_64.AppImage` (CLI batch tool, leaner) — using `linuxdeploy` + `linuxdeploy-plugin-qt`. Inline smoke tests T3 (batch `--version`) and T4 (gui `--help` under `QT_QPA_PLATFORM=minimal`).
|
||
|
||
- [ ] **Step 1: Write the script**
|
||
|
||
Create `scripts/build-appimages.sh`:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Build sethLabels AppImages (GUI + batch).
|
||
#
|
||
# Pipeline (spec §5.3):
|
||
# 1. sanity / guardrail / version-compute (same as build-deb.sh)
|
||
# 2. out-of-tree cmake build with CMAKE_INSTALL_PREFIX=/usr
|
||
# 3. cmake --install to staging AppDir
|
||
# 4. linuxdeploy bundle GUI AppImage
|
||
# 5. re-stage AppDir for batch-only, linuxdeploy bundle batch AppImage
|
||
# 6. inline smoke tests T3, T4
|
||
# 7. print artifact paths
|
||
#
|
||
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.3
|
||
set -euo pipefail
|
||
|
||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$REPO_ROOT"
|
||
|
||
echo "==> [1/6] Sanity check build host"
|
||
"$REPO_ROOT/scripts/lib/deps-debian.sh"
|
||
echo "==> [1/6] Strict-zero guardrail"
|
||
"$REPO_ROOT/scripts/check-no-upstream-edits.sh"
|
||
echo "==> [1/6] Compute version"
|
||
VERSION="$("$REPO_ROOT/scripts/compute-version.sh")"
|
||
echo " VERSION = $VERSION"
|
||
|
||
# Bootstrap linuxdeploy + plugin-qt; defines $LINUXDEPLOY_BIN and $LINUXDEPLOY_PLUGIN_QT_BIN.
|
||
# shellcheck disable=SC1091
|
||
source "$REPO_ROOT/scripts/lib/linuxdeploy.sh"
|
||
# linuxdeploy looks for the plugin in PATH; symlink into the cache dir suffices.
|
||
PLUGIN_DIR="$(dirname "$LINUXDEPLOY_PLUGIN_QT_BIN")"
|
||
PATH="$PLUGIN_DIR:$PATH"
|
||
# Plugin file must be named exactly `linuxdeploy-plugin-qt` (no version suffix).
|
||
PLUGIN_LINK="$PLUGIN_DIR/linuxdeploy-plugin-qt"
|
||
ln -sf "$LINUXDEPLOY_PLUGIN_QT_BIN" "$PLUGIN_LINK"
|
||
chmod +x "$PLUGIN_LINK"
|
||
|
||
echo "==> [2/6] Out-of-tree cmake build (install prefix /usr)"
|
||
BUILD_DIR="$REPO_ROOT/build/appimage"
|
||
APPDIR_GUI="$BUILD_DIR/AppDir-gui"
|
||
APPDIR_BATCH="$BUILD_DIR/AppDir-batch"
|
||
rm -rf "$BUILD_DIR"
|
||
mkdir -p "$BUILD_DIR"
|
||
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -G Ninja \
|
||
-DCMAKE_BUILD_TYPE=Release \
|
||
-DCMAKE_INSTALL_PREFIX=/usr
|
||
cmake --build "$BUILD_DIR" --parallel
|
||
|
||
echo "==> [3/6] Stage install tree to AppDirs"
|
||
DESTDIR="$APPDIR_GUI" cmake --install "$BUILD_DIR"
|
||
# Batch AppDir gets its own copy so we can prune Qt plugins that GUI needs but batch doesn't.
|
||
DESTDIR="$APPDIR_BATCH" cmake --install "$BUILD_DIR"
|
||
|
||
# Sanity: both AppDirs must contain both binaries (we strip later, not here).
|
||
test -x "$APPDIR_GUI/usr/bin/glabels-qt" || { echo "ERROR: GUI binary missing in AppDir-gui" >&2; exit 1; }
|
||
test -x "$APPDIR_BATCH/usr/bin/glabels-batch-qt" || { echo "ERROR: batch binary missing in AppDir-batch" >&2; exit 1; }
|
||
|
||
echo "==> [4/6] Bundle GUI AppImage"
|
||
DESKTOP_FILE="$APPDIR_GUI/usr/share/applications/glabels-qt.desktop"
|
||
ICON_FILE="$APPDIR_GUI/usr/share/icons/hicolor/scalable/apps/glabels.svg"
|
||
# Upstream's actual desktop filename may vary — list what's there if missing.
|
||
if [ ! -f "$DESKTOP_FILE" ]; then
|
||
ALT_DESKTOP=$(find "$APPDIR_GUI/usr/share/applications" -name '*.desktop' | head -1)
|
||
if [ -n "$ALT_DESKTOP" ]; then
|
||
DESKTOP_FILE="$ALT_DESKTOP"
|
||
else
|
||
echo "ERROR: no .desktop file found in $APPDIR_GUI/usr/share/applications" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
cd "$BUILD_DIR"
|
||
APPIMAGE_EXTRACT_AND_RUN=1 \
|
||
"$LINUXDEPLOY_BIN" \
|
||
--appdir "$APPDIR_GUI" \
|
||
--plugin qt \
|
||
--executable "$APPDIR_GUI/usr/bin/glabels-qt" \
|
||
--desktop-file "$DESKTOP_FILE" \
|
||
--icon-file "$ICON_FILE" \
|
||
--output appimage
|
||
|
||
GUI_RAW=$(ls "$BUILD_DIR"/*GUI*.AppImage "$BUILD_DIR"/*glabels-qt*.AppImage 2>/dev/null | head -1)
|
||
GUI_OUT="$REPO_ROOT/sethlabels-gui-${VERSION}-x86_64.AppImage"
|
||
mv "$GUI_RAW" "$GUI_OUT"
|
||
chmod +x "$GUI_OUT"
|
||
cd "$REPO_ROOT"
|
||
|
||
echo "==> [5/6] Bundle batch AppImage"
|
||
# Batch doesn't need a desktop file or icon (CLI only).
|
||
cd "$BUILD_DIR"
|
||
APPIMAGE_EXTRACT_AND_RUN=1 \
|
||
"$LINUXDEPLOY_BIN" \
|
||
--appdir "$APPDIR_BATCH" \
|
||
--plugin qt \
|
||
--executable "$APPDIR_BATCH/usr/bin/glabels-batch-qt" \
|
||
--create-desktop-file \
|
||
--output appimage
|
||
|
||
BATCH_RAW=$(ls "$BUILD_DIR"/*batch*.AppImage 2>/dev/null | head -1)
|
||
BATCH_OUT="$REPO_ROOT/sethlabels-batch-${VERSION}-x86_64.AppImage"
|
||
mv "$BATCH_RAW" "$BATCH_OUT"
|
||
chmod +x "$BATCH_OUT"
|
||
cd "$REPO_ROOT"
|
||
|
||
echo "==> [6/6] Smoke tests"
|
||
|
||
# T3: batch AppImage --version exits 0 with non-empty output.
|
||
echo " T3: batch --version"
|
||
T3_OUT=$(APPIMAGE_EXTRACT_AND_RUN=1 "$BATCH_OUT" --version 2>&1) || {
|
||
echo "ERROR: T3 failed — batch AppImage --version exited non-zero" >&2
|
||
echo "$T3_OUT" >&2
|
||
exit 1
|
||
}
|
||
if [ -z "$T3_OUT" ]; then
|
||
echo "ERROR: T3 failed — batch AppImage --version produced empty output" >&2
|
||
exit 1
|
||
fi
|
||
echo " T3: PASS ($(echo "$T3_OUT" | head -1))"
|
||
|
||
# T4: GUI AppImage --help exits 0 under headless Qt platform.
|
||
echo " T4: gui --help (QT_QPA_PLATFORM=minimal)"
|
||
APPIMAGE_EXTRACT_AND_RUN=1 QT_QPA_PLATFORM=minimal "$GUI_OUT" --help >/tmp/sethlabels-gui-help 2>&1 || {
|
||
echo "ERROR: T4 failed — GUI AppImage --help exited non-zero" >&2
|
||
cat /tmp/sethlabels-gui-help >&2
|
||
exit 1
|
||
}
|
||
echo " T4: PASS"
|
||
|
||
echo ""
|
||
echo "Artifacts:"
|
||
echo " $GUI_OUT"
|
||
echo " $BATCH_OUT"
|
||
```
|
||
|
||
```bash
|
||
chmod +x scripts/build-appimages.sh
|
||
```
|
||
|
||
- [ ] **Step 2: Run the build**
|
||
|
||
```bash
|
||
./scripts/build-appimages.sh
|
||
```
|
||
|
||
Expected: walks through all 6 steps, ends with two artifact paths. Wall time ~5 minutes on a modern machine. If `linuxdeploy` errors with "no $XDG_RUNTIME_DIR" or similar, set the env var: `export XDG_RUNTIME_DIR=/tmp/runtime-$USER && mkdir -p $XDG_RUNTIME_DIR`.
|
||
|
||
If T3 or T4 fails: see spec §F2 — Qt plugin omissions are the usual culprit. Re-run with `LINUXDEPLOY_OUTPUT_VERSION=$VERSION` and inspect the bundle's `usr/plugins/` dir for missing platform/imageformats plugins.
|
||
|
||
- [ ] **Step 3: Smoke-test the artifacts manually**
|
||
|
||
```bash
|
||
GUI_OUT=$(ls sethlabels-gui-*.AppImage | head -1)
|
||
BATCH_OUT=$(ls sethlabels-batch-*.AppImage | head -1)
|
||
ls -la "$GUI_OUT" "$BATCH_OUT"
|
||
APPIMAGE_EXTRACT_AND_RUN=1 "$BATCH_OUT" --version
|
||
APPIMAGE_EXTRACT_AND_RUN=1 QT_QPA_PLATFORM=minimal "$GUI_OUT" --help | head -10
|
||
```
|
||
|
||
Expected: GUI is ~50–80 MB (bundles Qt6), batch is ~20–40 MB (no GUI plugins). Both run cleanly.
|
||
|
||
- [ ] **Step 4: Verify .gitignore catches the AppImages**
|
||
|
||
```bash
|
||
git status
|
||
```
|
||
|
||
Expected: no `*.AppImage` files appear in the untracked list (matched by the `*.AppImage` rule added in Task 1 Step 3).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/build-appimages.sh
|
||
git commit -m "feat: add build-appimages.sh with inline smoke tests T3, T4"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: `scripts/README.md`
|
||
|
||
**Files:**
|
||
- Create: `scripts/README.md`
|
||
|
||
**What it does:** Operator-facing run instructions. Documents the order to run scripts, prerequisites, and where artifacts land. Mirrored as the canonical recipe for future CI YAML wrapping (spec I3).
|
||
|
||
- [ ] **Step 1: Write `scripts/README.md`**
|
||
|
||
```bash
|
||
cat > scripts/README.md <<'EOF'
|
||
# sethLabels build scripts
|
||
|
||
Canonical recipe for building sethLabels artifacts. CI YAML at the public-flip
|
||
will call these scripts unmodified — no logic moves into YAML (spec §I3).
|
||
|
||
## Quick reference
|
||
|
||
```
|
||
./scripts/lib/deps-debian.sh # check / install build deps
|
||
./scripts/check-no-upstream-edits.sh # enforce strict-zero (I1)
|
||
./scripts/compute-version.sh # emit <upstream-tag>-seth<N>
|
||
./scripts/build-deb.sh # → build/deb/glabels-qt_<VERSION>_amd64.deb
|
||
./scripts/build-appimages.sh # → sethlabels-{gui,batch}-<VERSION>-x86_64.AppImage
|
||
```
|
||
|
||
## Prerequisites
|
||
|
||
Debian 13 (Trixie) or Ubuntu 24.04 LTS. Run:
|
||
|
||
```
|
||
./scripts/lib/deps-debian.sh
|
||
```
|
||
|
||
If anything is missing, the script prints the exact `sudo apt install ...`
|
||
command to run.
|
||
|
||
`bats` (bash test framework) is in the dep list — it's required for the
|
||
implementation tests under `tests-impl/`.
|
||
|
||
`linuxdeploy` and `linuxdeploy-plugin-qt` are NOT apt-installable; they're
|
||
downloaded automatically by `scripts/lib/linuxdeploy.sh` to `scripts/.cache/`
|
||
on first AppImage build.
|
||
|
||
## Versioning
|
||
|
||
`<upstream-tag>-seth<N>` (e.g., `3.99-master618-seth1`). The `<N>` counter is
|
||
computed from existing git tags matching `<upstream-tag>-seth*`. See spec §D4.
|
||
|
||
**Caller responsibility:** the local tag db must be fresh before running
|
||
`compute-version.sh`. Run `git fetch origin --tags` first if you're not
|
||
inside the release flow (which fetches tags as step 1).
|
||
|
||
## Release flow
|
||
|
||
See spec §6 for the canonical step-by-step. TL;DR:
|
||
|
||
```
|
||
git fetch --all --tags
|
||
git rebase upstream/master
|
||
./scripts/check-no-upstream-edits.sh
|
||
./scripts/build-deb.sh # ~2 min
|
||
./scripts/build-appimages.sh # ~5 min
|
||
VERSION=$(./scripts/compute-version.sh)
|
||
git tag "$VERSION"
|
||
git push origin main --tags
|
||
# Create Gitea release for $VERSION; attach the three artifacts.
|
||
# Bump ../homebrew-tap/Formula/glabels-qt.rb (tag + revision); commit; push.
|
||
# Smoke verify on a clean Debian 13 VM (T5).
|
||
```
|
||
|
||
## Layout
|
||
|
||
```
|
||
scripts/
|
||
├── README.md ← this file
|
||
├── compute-version.sh ← pure logic; emits version string
|
||
├── check-no-upstream-edits.sh ← guardrail enforcing I1
|
||
├── build-deb.sh ← end-to-end .deb pipeline
|
||
├── build-appimages.sh ← end-to-end AppImage pipeline (GUI + batch)
|
||
├── lib/
|
||
│ ├── deps-debian.sh ← build-dep manifest + checker
|
||
│ └── linuxdeploy.sh ← linuxdeploy + plugin-qt bootstrapper
|
||
└── .cache/ ← gitignored; linuxdeploy AppImages cache
|
||
```
|
||
|
||
## Tests
|
||
|
||
```
|
||
./tests-impl/run-all.sh
|
||
```
|
||
|
||
Runs the bats suite for pure-logic scripts. Build-script smoke tests (T1–T4)
|
||
are inline in `build-deb.sh` and `build-appimages.sh` — they fire automatically
|
||
on each build.
|
||
|
||
## Spec
|
||
|
||
The design rationale, invariants, and failure modes live in
|
||
[`../sethlabels-docs/specs/2026-04-29-packaging-design.md`](../sethlabels-docs/specs/2026-04-29-packaging-design.md).
|
||
Read it before changing any script.
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 2: Verify the README renders sensibly**
|
||
|
||
```bash
|
||
head -40 scripts/README.md
|
||
```
|
||
|
||
Expected: clean Markdown, no obvious typos.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add scripts/README.md
|
||
git commit -m "docs: add scripts/README.md (operator run guide)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: `README.sethlabels.md` (repo-root entry)
|
||
|
||
**Files:**
|
||
- Create: `README.sethlabels.md`
|
||
|
||
**What it does:** Repo-root sethLabels entry point. Names the fork's purpose, points readers at install methods, and links to the upstream README and design spec. Strict-zero forbids modifying upstream `README.md`, hence the `.sethlabels.md` suffix.
|
||
|
||
- [ ] **Step 1: Write `README.sethlabels.md`**
|
||
|
||
```bash
|
||
cat > README.sethlabels.md <<'EOF'
|
||
# sethLabels
|
||
|
||
> Deployment fork of [glabels-qt](https://github.com/j-evins/glabels-qt) — Qt6
|
||
> label designer / printer, packaged for Debian-family Linux and macOS.
|
||
|
||
This is **not** a code fork. The upstream application is unchanged; sethLabels
|
||
exists solely to publish installable binary artifacts that upstream explicitly
|
||
does not provide ("Currently there are no self-hosted binary snapshot releases
|
||
available… I encourage you to try building the code yourself" — upstream README).
|
||
|
||
For the application itself — what it does, screenshots, full feature list — see
|
||
the upstream [`README.md`](README.md).
|
||
|
||
## Install
|
||
|
||
### Debian / Ubuntu (`.deb`)
|
||
|
||
Download the latest `.deb` from the [releases page](https://git.sethpc.xyz/Seth/sethLabels/releases),
|
||
then:
|
||
|
||
```
|
||
sudo apt install ./glabels-qt_<VERSION>_amd64.deb
|
||
glabels-qt --version
|
||
```
|
||
|
||
### Any Linux (AppImage)
|
||
|
||
Download `sethlabels-gui-<VERSION>-x86_64.AppImage` from the [releases page](https://git.sethpc.xyz/Seth/sethLabels/releases),
|
||
make it executable, and run it:
|
||
|
||
```
|
||
chmod +x sethlabels-gui-<VERSION>-x86_64.AppImage
|
||
./sethlabels-gui-<VERSION>-x86_64.AppImage
|
||
```
|
||
|
||
A separate `sethlabels-batch-<VERSION>-x86_64.AppImage` provides the CLI for
|
||
scripted / mail-merge use.
|
||
|
||
### macOS (Homebrew)
|
||
|
||
```
|
||
brew tap seth/tap https://git.sethpc.xyz/Seth/homebrew-tap.git
|
||
brew install seth/tap/glabels-qt
|
||
```
|
||
|
||
The explicit URL form is needed because brew defaults to GitHub for tap names.
|
||
First install builds Qt6 + glabels-qt from source (~5–10 min one-time cost; see
|
||
spec §D2). Subsequent updates are a fast `brew upgrade`.
|
||
|
||
## Build from source
|
||
|
||
If you'd rather build the artifacts yourself instead of downloading a release:
|
||
|
||
```
|
||
git clone https://git.sethpc.xyz/Seth/sethLabels.git
|
||
cd sethLabels
|
||
./scripts/lib/deps-debian.sh # check / install build deps
|
||
./scripts/build-deb.sh # → build/deb/glabels-qt_*.deb
|
||
./scripts/build-appimages.sh # → sethlabels-{gui,batch}-*.AppImage
|
||
```
|
||
|
||
See [`scripts/README.md`](scripts/README.md) for full operator docs.
|
||
|
||
## How this fork works
|
||
|
||
sethLabels is a **deployment fork**: every sethLabels addition lives in NEW
|
||
files in NEW top-level directories (`scripts/`, `packaging/`,
|
||
`sethlabels-docs/`, `tests-impl/`, plus this file). Upstream files are never
|
||
edited. The single allowlisted exception is `.gitignore`. This discipline is
|
||
enforced by `scripts/check-no-upstream-edits.sh`.
|
||
|
||
The `<upstream-tag>-seth<N>` versioning preserves the upstream-lineage in every
|
||
artifact. Periodic `git rebase upstream/master` is conflict-free by construction.
|
||
|
||
## Spec & decisions
|
||
|
||
- [Design spec](sethlabels-docs/specs/2026-04-29-packaging-design.md) — invariants, decisions, build pipeline, failure modes
|
||
- [Decision log](DECISIONS.md) — settled choices + rejected alternatives
|
||
- [Project brief](IDEA.md) — plain-language motivation
|
||
|
||
## License
|
||
|
||
The upstream code is licensed under [GPL-3.0](LICENSE). sethLabels-specific
|
||
files (everything in the dirs listed above, plus this file) are licensed under
|
||
the same terms.
|
||
|
||
## Upstream
|
||
|
||
- Upstream: https://github.com/j-evins/glabels-qt (Jaye Evins / glabels.org)
|
||
- This fork: https://git.sethpc.xyz/Seth/sethLabels
|
||
- Brew tap: https://git.sethpc.xyz/Seth/homebrew-tap
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 2: Verify guardrail still passes**
|
||
|
||
```bash
|
||
./scripts/check-no-upstream-edits.sh && echo CLEAN
|
||
```
|
||
|
||
Expected: `CLEAN`. `README.sethlabels.md` is in the allowlist (added in Task 3 Step 3's `allowed_pattern`).
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add README.sethlabels.md
|
||
git commit -m "docs: add README.sethlabels.md (fork entry point)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: Homebrew tap repo (separate Gitea repo)
|
||
|
||
**Files (in a separate repo at `~/bin/homebrew-tap/`):**
|
||
- Create: `~/bin/homebrew-tap/Formula/glabels-qt.rb`
|
||
- Create: `~/bin/homebrew-tap/README.md`
|
||
- Create: Gitea repo `git.sethpc.xyz/Seth/homebrew-tap`
|
||
|
||
**What it does:** Provides the macOS install path per spec §7 + §D2. Build-from-source on the user's Mac via brew.
|
||
|
||
This task does NOT modify the sethLabels repo. It creates a parallel sibling repo.
|
||
|
||
- [ ] **Step 1: Create the local repo skeleton**
|
||
|
||
```bash
|
||
mkdir -p ~/bin/homebrew-tap/Formula
|
||
cd ~/bin/homebrew-tap
|
||
git init -q -b main
|
||
git config user.email "seth@sethfreiberg.com"
|
||
git config user.name "Seth Freiberg"
|
||
```
|
||
|
||
- [ ] **Step 2: Create `Formula/glabels-qt.rb`**
|
||
|
||
```bash
|
||
cat > Formula/glabels-qt.rb <<'EOF'
|
||
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: "PLACEHOLDER_FILLED_AT_FIRST_RELEASE",
|
||
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
|
||
EOF
|
||
```
|
||
|
||
The `tag:` and `revision:` placeholders are filled at the first release (Task 12 Step 6).
|
||
|
||
- [ ] **Step 3: Create `README.md` for the tap**
|
||
|
||
```bash
|
||
cat > README.md <<'EOF'
|
||
# Seth's Homebrew tap
|
||
|
||
Homebrew tap publishing macOS install for [sethLabels](https://git.sethpc.xyz/Seth/sethLabels).
|
||
|
||
## Install
|
||
|
||
```
|
||
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 Homebrew defaults to GitHub for tap
|
||
names. When this repo is mirrored to GitHub at the public-flip, the URL becomes
|
||
implicit and the tap command shortens to `brew tap seth/tap`.
|
||
|
||
## Formulae
|
||
|
||
| Formula | Description |
|
||
|---------|-------------|
|
||
| `glabels-qt` | [gLabels label designer (Qt/C++)](https://git.sethpc.xyz/Seth/sethLabels) — Seth's packaging fork of glabels-qt |
|
||
|
||
## How this works
|
||
|
||
`brew install seth/tap/glabels-qt` clones the sethLabels git tag pinned in
|
||
`Formula/glabels-qt.rb`, builds Qt6 + glabels-qt from source, and installs to
|
||
`/opt/homebrew/`. First install takes ~5–10 minutes. Subsequent
|
||
`brew upgrade glabels-qt` runs are fast (only the version-bumped formula
|
||
re-builds).
|
||
|
||
## Per-release maintenance
|
||
|
||
Each sethLabels release is one commit on this repo: bump `tag:` and `revision:`
|
||
in `Formula/glabels-qt.rb`. No other edits expected.
|
||
|
||
## Spec
|
||
|
||
Design rationale lives in the sethLabels repo:
|
||
[`sethlabels-docs/specs/2026-04-29-packaging-design.md`](https://git.sethpc.xyz/Seth/sethLabels/src/branch/main/sethlabels-docs/specs/2026-04-29-packaging-design.md) §D2, §7.
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Step 4: Initial commit on local tap repo**
|
||
|
||
```bash
|
||
cd ~/bin/homebrew-tap
|
||
git add Formula/glabels-qt.rb README.md
|
||
git commit -m "chore: scaffold tap with glabels-qt formula"
|
||
```
|
||
|
||
- [ ] **Step 5: Create Gitea remote and push**
|
||
|
||
Use the `gitea` CLI per global instructions:
|
||
|
||
```bash
|
||
cd ~/bin/homebrew-tap
|
||
gitea create homebrew-tap --description "Homebrew tap publishing macOS install for sethLabels"
|
||
gitea remote homebrew-tap
|
||
gitea push
|
||
```
|
||
|
||
Expected: `gitea create` prints the new repo URL; `gitea remote` sets `origin`; `gitea push` pushes `main`.
|
||
|
||
- [ ] **Step 6: Verify the tap can be browsed**
|
||
|
||
```bash
|
||
curl -s https://git.sethpc.xyz/Seth/homebrew-tap/raw/branch/main/Formula/glabels-qt.rb | head -10
|
||
```
|
||
|
||
Expected: prints the first 10 lines of the formula.
|
||
|
||
- [ ] **Step 7: Return to sethLabels repo**
|
||
|
||
```bash
|
||
cd ~/bin/sethLabels
|
||
```
|
||
|
||
The sethLabels repo has no commits in this task — the tap is a separate repo. The next task is the first end-to-end release of sethLabels itself.
|
||
|
||
---
|
||
|
||
## Task 12: First end-to-end release dry run (operator checklist)
|
||
|
||
**Files modified:**
|
||
- Modify: `~/bin/homebrew-tap/Formula/glabels-qt.rb` (Step 9 — replace placeholder tag and revision)
|
||
- Modify: `packaging/changelog.md` (Step 5 — add the seth1 entry)
|
||
|
||
**What it does:** Walks the spec §6 release flow end-to-end. Produces real artifacts, tags the sethLabels repo, attaches artifacts to a Gitea release, bumps the brew formula, and (optionally) verifies T5 install on a clean Debian 13 VM.
|
||
|
||
- [ ] **Step 1: Refresh tags + rebase**
|
||
|
||
```bash
|
||
cd ~/bin/sethLabels
|
||
git fetch --all --tags
|
||
git rebase upstream/master
|
||
```
|
||
|
||
Expected: rebase is conflict-free (strict-zero invariant). If it conflicts, STOP — something has slipped past the guardrail.
|
||
|
||
- [ ] **Step 2: Run guardrail explicitly**
|
||
|
||
```bash
|
||
./scripts/check-no-upstream-edits.sh && echo CLEAN
|
||
```
|
||
|
||
Expected: `CLEAN`.
|
||
|
||
- [ ] **Step 3: Build the .deb**
|
||
|
||
```bash
|
||
./scripts/build-deb.sh
|
||
```
|
||
|
||
Expected: ends with `Artifact: build/deb/glabels-qt_<VERSION>_amd64.deb`. Note the version string for use in later steps.
|
||
|
||
- [ ] **Step 4: Build the AppImages**
|
||
|
||
```bash
|
||
./scripts/build-appimages.sh
|
||
```
|
||
|
||
Expected: ends with two `Artifacts:` lines naming the GUI and batch AppImages.
|
||
|
||
- [ ] **Step 5: Update `packaging/changelog.md`**
|
||
|
||
Edit `packaging/changelog.md` to convert the `## (unreleased)` block to a real version block. Replace the existing `## (unreleased)` section with:
|
||
|
||
```markdown
|
||
## <VERSION> — <YYYY-MM-DD>
|
||
|
||
- First end-to-end release of sethLabels packaging pipeline.
|
||
- `.deb` produced via CMake CPack with strict-zero `-D` overrides (no upstream edits).
|
||
- AppImages (GUI + batch) bundled via linuxdeploy + linuxdeploy-plugin-qt, pinned per F9.
|
||
- Brew tap initial publish at `git.sethpc.xyz/Seth/homebrew-tap`.
|
||
|
||
## (unreleased)
|
||
|
||
- (no changes since <VERSION>)
|
||
```
|
||
|
||
Substitute `<VERSION>` with the value from Step 3 and `<YYYY-MM-DD>` with today's ISO date.
|
||
|
||
```bash
|
||
git add packaging/changelog.md
|
||
git commit -m "docs: changelog for <VERSION>"
|
||
```
|
||
|
||
- [ ] **Step 6: Compute and tag**
|
||
|
||
```bash
|
||
VERSION=$(./scripts/compute-version.sh)
|
||
echo "Tagging $VERSION"
|
||
git tag "$VERSION"
|
||
git push origin main --tags
|
||
```
|
||
|
||
Expected: tag pushes successfully. If the `seth<N>` count is unexpectedly high, you forgot the `git fetch --all --tags` in Step 1 or there are leftover tags from local experiments.
|
||
|
||
- [ ] **Step 7: Create Gitea release with artifacts attached**
|
||
|
||
Use the Gitea API per `~/bin/GITEA_API.md`:
|
||
|
||
```bash
|
||
DEB=$(ls build/deb/glabels-qt_*.deb | head -1)
|
||
GUI=$(ls sethlabels-gui-*.AppImage | head -1)
|
||
BATCH=$(ls sethlabels-batch-*.AppImage | head -1)
|
||
|
||
# Read token from the standard location.
|
||
GITEA_TOKEN=$(cat ~/.config/gitea/token)
|
||
GITEA_BASE="https://git.sethpc.xyz/api/v1"
|
||
|
||
# Create the release.
|
||
RELEASE_JSON=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"tag_name\":\"$VERSION\",\"name\":\"sethLabels $VERSION\",\"body\":\"See packaging/changelog.md for notes.\"}" \
|
||
"$GITEA_BASE/repos/Seth/sethLabels/releases")
|
||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||
echo "Created release ID $RELEASE_ID"
|
||
|
||
# Attach all three artifacts.
|
||
for f in "$DEB" "$GUI" "$BATCH"; do
|
||
echo "Attaching $f"
|
||
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||
-F "attachment=@${f}" \
|
||
"$GITEA_BASE/repos/Seth/sethLabels/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
||
>/dev/null
|
||
done
|
||
|
||
echo "Release URL: https://git.sethpc.xyz/Seth/sethLabels/releases/tag/$VERSION"
|
||
```
|
||
|
||
Expected: prints the release URL; opening it shows the three attachments.
|
||
|
||
- [ ] **Step 8: Verify download URLs are public**
|
||
|
||
```bash
|
||
curl -sI "https://git.sethpc.xyz/Seth/sethLabels/releases/download/$VERSION/$(basename "$DEB")" | head -1
|
||
```
|
||
|
||
Expected: `HTTP/2 200` or `HTTP/2 302` (redirect to the asset).
|
||
|
||
- [ ] **Step 9: Bump `homebrew-tap/Formula/glabels-qt.rb`**
|
||
|
||
```bash
|
||
cd ~/bin/homebrew-tap
|
||
TAG="$VERSION"
|
||
REVISION=$(cd ~/bin/sethLabels && git rev-list -n 1 "$VERSION")
|
||
echo "Pinning tag=$TAG revision=$REVISION"
|
||
|
||
# Replace placeholders.
|
||
sed -i "s|tag: \"PLACEHOLDER_FILLED_AT_FIRST_RELEASE\"|tag: \"$TAG\"|" Formula/glabels-qt.rb
|
||
sed -i "s|revision: \"PLACEHOLDER_FILLED_AT_FIRST_RELEASE\"|revision: \"$REVISION\"|" Formula/glabels-qt.rb
|
||
|
||
# Verify the file is well-formed Ruby (parse-only check; doesn't run Homebrew).
|
||
ruby -c Formula/glabels-qt.rb
|
||
|
||
git diff Formula/glabels-qt.rb
|
||
git add Formula/glabels-qt.rb
|
||
git commit -m "bump glabels-qt to $TAG"
|
||
git push origin main
|
||
```
|
||
|
||
Expected: `ruby -c` reports `Syntax OK`; `git push` succeeds.
|
||
|
||
- [ ] **Step 10: Optional — T5 fresh-VM smoke test**
|
||
|
||
Per spec §10, T5 is "install on a clean Debian 13 VM and run `glabels-qt --version`". This is the strongest signal that `dpkg-shlibdeps` produced a correct depends list. Recommended on every `seth1` release; skippable on `seth2`+ packaging-only fixes.
|
||
|
||
If you have a clean Debian 13 VM available:
|
||
|
||
```
|
||
# On the clean VM:
|
||
wget https://git.sethpc.xyz/Seth/sethLabels/releases/download/<VERSION>/glabels-qt_<VERSION>_amd64.deb
|
||
sudo apt install -y ./glabels-qt_<VERSION>_amd64.deb
|
||
glabels-qt --version
|
||
```
|
||
|
||
Expected: install succeeds; `--version` exits 0 and prints the version string.
|
||
|
||
If `apt install` errors with unmet deps, the `dpkg-shlibdeps` calculation was wrong (spec §F8). Mitigation: override via `CPACK_DEBIAN_PACKAGE_DEPENDS` in `scripts/build-deb.sh`, rebuild, re-tag as `seth2`.
|
||
|
||
- [ ] **Step 11: Update CLAUDE.md to reflect post-implementation state**
|
||
|
||
```bash
|
||
cd ~/bin/sethLabels
|
||
```
|
||
|
||
Open `CLAUDE.md`, find the `## Current State` block, and replace its content with:
|
||
|
||
```markdown
|
||
## Current State
|
||
|
||
- **Phase:** post-first-release. Pipeline live. First tag: <VERSION>. Three artifacts attached to the Gitea release. Brew tap bumped to match.
|
||
- **Repo:** `git.sethpc.xyz/Seth/sethLabels` (default branch `main`). Tap: `git.sethpc.xyz/Seth/homebrew-tap`. Upstream: `j-evins/glabels-qt` (`upstream` remote).
|
||
- **Deploy targets live:** Debian-family Linux (`.deb` + AppImage) and macOS via Homebrew tap.
|
||
- **Next release:** rebase, build, tag, attach, bump tap. See `scripts/README.md` and spec §6.
|
||
```
|
||
|
||
```bash
|
||
git add CLAUDE.md
|
||
git commit -m "docs: refresh CLAUDE.md to post-first-release phase"
|
||
git push origin main
|
||
```
|
||
|
||
- [ ] **Step 12: Write a session handoff**
|
||
|
||
Per Seth's global persistence convention, create a handoff document capturing the session's outcome:
|
||
|
||
```bash
|
||
# Use the session-handoff skill to create the handoff document with proper structure.
|
||
# (See ~/.claude/CLAUDE.md → Persistence Partition → Session close).
|
||
```
|
||
|
||
The handoff filename pattern is `.claude/handoffs/YYYY-MM-DD-HHMMSS-first-release.md`.
|
||
|
||
---
|
||
|
||
## Self-review checklist
|
||
|
||
After all 12 tasks complete, the implementer should verify:
|
||
|
||
- [ ] **Spec coverage:** every section of `sethlabels-docs/specs/2026-04-29-packaging-design.md` maps to a task — §2 (invariants enforced by Task 3); §3 (decisions reflected throughout); §4 (file structure produced); §5.1–5.5 (Tasks 4–7 in order); §6 (Task 12); §7 (Task 11); §F1–F9 (each guarded somewhere — F1=Task 3, F2=Task 8 inline T4, F3=Task 4, F4=Task 11, F5=Task 2, F6=Task 9, F7=Task 12 step 1 rebase verification, F8=Task 12 step 10 T5, F9=Task 5 explicit pinning); §10 (T1–T4 inline; T5 in Task 12 step 10).
|
||
- [ ] **No upstream files modified:** `git diff --name-only upstream/master..HEAD` shows only allowlisted paths.
|
||
- [ ] **All bats tests pass:** `./tests-impl/run-all.sh` reports green.
|
||
- [ ] **The .deb installs cleanly** on a fresh Debian 13 VM and `glabels-qt --version` exits 0.
|
||
- [ ] **Both AppImages run** under `APPIMAGE_EXTRACT_AND_RUN=1` (no FUSE dependency).
|
||
- [ ] **Brew formula parses:** `ruby -c Formula/glabels-qt.rb` reports `Syntax OK`. (Live `brew install` test on a Mac is recommended but not gate-blocking.)
|
||
- [ ] **Release flow steps 1–9 of spec §6 ran cleanly** with no manual deviations.
|