From b7281e0f046f59e5c87a069f4340a8c5bff987e5 Mon Sep 17 00:00:00 2001 From: Seth Freiberg Date: Wed, 29 Apr 2026 12:19:24 -0400 Subject: [PATCH] docs: add session handoff (mac-app-launcher) --- .../2026-04-29-121529-mac-app-launcher.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .claude/handoffs/2026-04-29-121529-mac-app-launcher.md diff --git a/.claude/handoffs/2026-04-29-121529-mac-app-launcher.md b/.claude/handoffs/2026-04-29-121529-mac-app-launcher.md new file mode 100644 index 0000000..bdb2d44 --- /dev/null +++ b/.claude/handoffs/2026-04-29-121529-mac-app-launcher.md @@ -0,0 +1,167 @@ +# Handoff: macOS Launchpad/Spotlight integration via stub .app wrapper added to brew tap + +## Session Metadata +- Created: 2026-04-29 12:15:29 UTC +- Project: /home/claude/bin/sethLabels +- Branch: main +- Session duration: continuation of the same session that produced the first-release handoff (2026-04-29-155439); this addition is ~1 hour of follow-up work + +### Recent Commits (for context) + - f6c30f2 test: move bats scratch dirs to repo-local .test-scratch/ (per global no-/tmp/ rule) + - 76a3cb7 docs: README.sethlabels.md — include upstream remote setup in build-from-source + - 891cc7c docs: add session handoff (first-release) + - 2d04943 docs: refresh CLAUDE.md to post-first-release phase + - 2108e2c docs: changelog for 3.99-master618-seth1 + +Plus pending uncommitted edits this session (will be committed by the wrap-up step): + - CLAUDE.md (Conventions section: removed stale "to be implemented" wording for check-no-upstream-edits.sh; added tests-impl/ to dirs list) + - DECISIONS.md (appended .app launcher decision) + +Brew tap repo (`git.sethpc.xyz/Seth/homebrew-tap`) has TWO new commits this session: + - ef4d6c7 feat: generate stub .app bundle for Launchpad/Spotlight integration on macOS + - 3542762 fix: use upstream SVG (not nonexistent PNG) for .app icon conversion + +## Handoff Chain + +- **Continues from**: [2026-04-29-155439-first-release.md](./2026-04-29-155439-first-release.md) + - Previous title: sethLabels packaging pipeline live — first release published +- **Supersedes**: None. + +> The predecessor captures the 12-task implementation + first-release publication. This handoff extends that work with a single follow-up feature: macOS Launchpad/Spotlight integration. No regressions in the predecessor's deliverables; everything from the first release is unchanged and still live. + +## Current State Summary + +This session continued from "first release published" state. Seth asked whether the brew install would put a glabels-qt launcher icon in the Mac menu/Launchpad. Answer: no, because upstream's `glabels/CMakeLists.txt:125` declares `add_executable(glabels-qt WIN32 ...)` with no `MACOSX_BUNDLE` keyword, producing a CLI-only Mach-O on macOS. Strict-zero (I1) forbids patching upstream to fix this. Seth picked option 1 (stub `.app` wrapper synthesized in the brew formula) over option 2 (upstream a PR) and option 3 (switch to a Cask). Implemented as two commits on the brew tap repo. The sethLabels repo itself was NOT modified — the entire feature lives in the tap's `def install` block. + +The `.app` launcher works by: (1) cmake installs binaries to `bin/` per upstream rules, (2) brew formula's `def install` then synthesizes `/glabels-qt.app/Contents/{Info.plist, MacOS/glabels-qt, Resources/glabels-qt.icns}` where the launcher script is a 2-line shell that `exec`s the real CLI binary, (3) the icon is converted from upstream's installed SVG via macOS-built-in `sips`, (4) the formula's `caveats` block tells the user to run `cp -R "$(brew --prefix glabels-qt)/glabels-qt.app" /Applications/` once. + +NOT YET VALIDATED: needs first install on a real Mac. Two unknowns: whether macOS 13+ `sips` actually accepts SVG input (begin/rescue catches the failure with an `opoo` if not — falls back to generic icon), and whether Gatekeeper requires a right-click→Open on first launch (likely yes since the .app isn't signed, but acceptable for the project's audience). + +## Codebase Understanding + +### Architecture Overview + +The brew tap pattern: a Homebrew formula's `def install` runs on the user's Mac during `brew install`. It can do anything the user's shell can do, including writing files outside the source tree (within the Cellar prefix). This is the legitimate sethLabels-side hook for filling gaps that strict-zero forbids fixing in upstream code. The .app wrapper is one such gap; others (e.g., a future macOS-specific `Info.plist` content type registration for `.glabels` files) could plug in here too. + +The launcher script approach (`exec "#{bin}/glabels-qt" "$@"`) is a thin wrapper, not a copy. It avoids: +- Having to `MACOSX_BUNDLE` the cmake target (needs upstream patch — strict-zero forbids) +- Running `macdeployqt` on the binary (needs Mac to build — defeats decision D2's no-Mac-CI goal) +- Maintaining a separate Cask with a pre-built artifact (same problem) + +The wrapper has one downside: launching from Launchpad doesn't inherit a shell PATH, so the wrapper's `exec` path is hardcoded at install time using brew's `#{bin}` interpolation. That `#{bin}` resolves to `/opt/homebrew/Cellar/glabels-qt//bin/` on Apple Silicon, which has rpath set up correctly for Qt6 lookup. Should "just work" without `DYLD_LIBRARY_PATH` or `QT_PLUGIN_PATH` exports. + +### Critical Files + +| File | Purpose | Relevance | +|------|---------|-----------| +| `~/bin/homebrew-tap/Formula/glabels-qt.rb` | The brew formula. Contains `def install` (cmake build + .app synthesis), `def caveats` (user-facing post-install message), and `test do` (assert `--version` works). | This is the ONLY file the user changes per release — bump `tag:` and `revision:` (and the SHA needs `# pragma: allowlist secret` to bypass the tap repo's detect-secrets pre-commit hook). | +| `~/bin/homebrew-tap/README.md` | Tap install instructions. | Has a "Launchpad / Spotlight integration (macOS)" section documenting the one-time `cp -R` step. | +| `glabels/CMakeLists.txt` (upstream — DO NOT EDIT) | Lines 125 (`add_executable(glabels-qt WIN32 ...)` — no MACOSX_BUNDLE) and 152-156 (icon install rules) explain why we need the wrapper and where the SVG icon comes from. | Read-only — strict-zero forbids edits. The reason we need the .app wrapper. | +| `~/bin/sethLabels/sethlabels-docs/specs/2026-04-29-packaging-design.md` | Design spec. §D2 covers the brew tap decision; the .app wrapper is a sub-decision under it. | Reference for why brew tap was chosen over .dmg/Cask. | +| `~/bin/sethLabels/DECISIONS.md` | Project decision log. Has a new entry for the .app launcher choice. | Quick scan to see what was already decided/rejected. | + +### Key Patterns Discovered + +- **`on_macos do ... end`** — Homebrew DSL block for macOS-only formula logic. Wraps both the .app generation (in `def install`) and the user message (in `def caveats`). On Linux brew (rare for this formula), the .app code is skipped entirely. +- **Path interpolation in heredocs** — the launcher script uses `#{bin}/glabels-qt` interpolation. At formula evaluation time `#{bin}` becomes the absolute Cellar path. So the launcher script written to disk has a hardcoded full path, not an env-dependent reference. +- **`sips` for SVG → icns** — macOS-built-in tool. Wrapped in `begin/rescue` so a sips failure on older macOS (which can't read SVG) just `opoo`s and the .app gets a generic icon — no error path. +- **detect-secrets in tap repo** — the tap has a detect-secrets pre-commit hook that flags 40-char hex strings as secrets. The `revision:` field (a git SHA) gets a `# pragma: allowlist secret` inline comment. Every future tap bump must preserve that pragma. Future improvement: add the SHA pattern to `.secrets.baseline` so the pragma isn't needed. + +## Work Completed + +### Tasks Finished + +- [x] Diagnosed the menu-launcher question by reading upstream `glabels/CMakeLists.txt:125,150` (no `MACOSX_BUNDLE`) and `docs/BUILD-INSTRUCTIONS-MACOS.md` (CLI-only `make install`) +- [x] Presented three implementation options to Seth; he picked option 1 (stub .app via brew formula) +- [x] Implementer subagent generated the .app wrapper block in `def install` + `caveats` method + README.md "Launchpad / Spotlight" section. Committed as `ef4d6c7` on the tap. +- [x] Caught a bug via context check: the initial implementation globbed for `glabels.png` but upstream installs only SVGs (`glabels/CMakeLists.txt:152-156`). The `opoo` fallback would have fired on every install, leaving every .app with a generic icon. +- [x] Fix subagent updated the glob to `glabels.svg`, prefer the scalable variant, fall back to sized variants, wrap `sips` in `begin/rescue`. Committed as `3542762` on the tap. +- [x] CLAUDE.md fixed: removed stale "(to be implemented per spec §5.5)" wording for the guardrail; added `tests-impl/` to the dirs-list (uncommitted at handoff time). +- [x] DECISIONS.md appended: macOS Launchpad/Spotlight integration via stub .app wrapper, with rationale (uncommitted at handoff time). + +### Files Modified + +| File | Changes | Rationale | +|------|---------|-----------| +| `CLAUDE.md` (sethLabels) | Removed stale "(to be implemented per spec §5.5)" parenthetical; expanded the "Enforced by..." line to mention working-tree drift + ref-existence check; added `tests-impl/` to dirs list. | Stale wording flagged by cumulative review last session; tests-impl/ omission was a small accuracy gap. | +| `DECISIONS.md` (sethLabels) | Appended decision entry for the .app launcher approach. | Project's Decision-Log convention: every non-obvious settled choice gets logged. | +| `~/bin/homebrew-tap/Formula/glabels-qt.rb` | Added `on_macos do` block in `def install` to synthesize the .app; added `def caveats`; later fixed the icon glob from PNG to SVG. | Two commits — ef4d6c7 (initial) + 3542762 (icon fix). | +| `~/bin/homebrew-tap/README.md` | New "Launchpad / Spotlight integration (macOS)" subsection under `## Install`. | User-facing docs for the one-time `cp -R` step. | + +### Decisions Made + +| Decision | Options Considered | Rationale | +|----------|-------------------|-----------| +| .app launcher via brew formula `def install` synthesis | (1) Brew formula synthesis, (2) Upstream PR adding MACOSX_BUNDLE, (3) Switch from Formula to Cask with pre-built .app | Option 1 lives entirely sethLabels-side, no upstream dep, no Mac CI required. Option 2 indefinite timeline (upstream review). Option 3 needs a Mac to build the .app (defeats D2's no-macOS-CI simplification). | +| Icon source = upstream SVG, converted to icns via `sips` | (a) Skip icon (generic), (b) Convert SVG via sips, (c) Convert PNG via sips, (d) Ship pre-built .icns in tap repo, (e) Fetch .icns as a brew resource | (b) chosen. Upstream installs only SVGs — verified at `glabels/CMakeLists.txt:152-156`. (c) was the initial implementation's bug. (a) is the rescue-block fallback. (d)/(e) add tap-repo maintenance. | +| Don't auto-link .app to /Applications/ | (i) Manual `cp -R` (user runs once), (ii) Symlink during install, (iii) Use `brew linkapps` | (i) chosen. (ii) requires writing to `/Applications/` which brew formulas can't do without sudo. (iii) is deprecated. The caveats block reminds the user. | + +## Immediate Next Steps + +1. **First Mac install validation.** When you (or Seth) get to a Mac with brew, run `brew tap seth/tap https://git.sethpc.xyz/Seth/homebrew-tap.git && brew install seth/tap/glabels-qt`. Expected: build succeeds (~5-10 min first time), `which glabels-qt` finds the binary, `cp -R "$(brew --prefix glabels-qt)/glabels-qt.app" /Applications/` succeeds, the app appears in Launchpad and is searchable in Spotlight. If `sips` failed silently (older macOS), the icon will be generic — check Launchpad first. +2. **First-launch Gatekeeper handling.** macOS will likely block first launch with "cannot be opened because it is from an unidentified developer." Right-click → Open is the standard bypass. Document this in the tap README if it turns out to be a notable friction point. +3. **T5 fresh-Debian-13-VM smoke test for the .deb** (still deferred from the first-release handoff — also not yet done). Spin up a clean Debian 13 VM, download the .deb from the release page, install with `apt install ./...`, run `glabels-qt --version`. If unmet deps surface, override via `CPACK_DEBIAN_PACKAGE_DEPENDS` in `scripts/build-deb.sh` and re-tag as -seth2. + +## Blockers / Open Questions + +- **Does `sips` on the user's macOS version handle SVG input?** macOS 13+ should; older versions may fail. The `rescue` block falls back gracefully — the only signal will be a generic Mac icon in Launchpad. +- **Will Gatekeeper friction be acceptable to Seth's eventual users?** First-launch right-click→Open is standard for unsigned apps. If it becomes a nuisance, options are: (a) self-sign with a free Apple ID (no notarization, only works for personal use), (b) properly sign + notarize ($99/year — explicitly rejected in §D2), (c) document the right-click→Open step prominently. + +## Deferred Items + +- **Adding the SHA pattern to `.secrets.baseline`** in the tap repo — would eliminate the need for `# pragma: allowlist secret` on every future tap bump. Trivial change but I left it for a future session since it's not blocking. +- **Verifying the `.app` actually contains all needed Qt6 plugins on user-side** — brew's Qt6 ships with cocoa platform plugin, image format plugins, and SVG support. The wrapper's `exec` of the real binary picks them up via the binary's rpath. Should "just work" but unconfirmed without a Mac test. +- **Cumulative-review minor follow-ups** still deferred from previous handoff: build-appimages.sh step counter `[1/6]` reused 3x, `packaging/appimage-recipe.env` is currently unwired, `compute-version.sh` opaque error if upstream has no annotated tags, changelog.md not discoverable from README. + +## Important Context + +**The .app wrapper is the FIRST piece of sethLabels-side macOS-specific build logic.** Up to this point, sethLabels was fully cross-platform-by-virtue-of-strict-zero — every script was Linux-only because the targets were Linux-only. This session adds the first piece of mac-conditional code, but ONLY on the brew-tap side (the sethLabels repo itself remains Linux-targeted). Future macOS-specific gaps (e.g., `.glabels` UTI registration, dock badge support, etc.) should follow the same pattern: handled in the brew formula's `def install` and `def caveats`, never via patches to upstream code. + +**The tap's detect-secrets hook is a known papercut.** Every release-flow run will require manually preserving the `# pragma: allowlist secret` comment on the `revision:` line after `sed`-replacing the SHA. The cleanest fix is to add the SHA pattern to `.secrets.baseline` (a `pre-commit run --all-files` regenerate after a `pre-commit autoupdate`). Until then, the release flow's tap-bump step should warn about this. + +**Don't conflate the .app wrapper with a "real" Mac app.** It's a thin shell script in `Contents/MacOS/`. Apps that need to listen for system events, register URL schemes, or claim file types properly need a real bundle with proper Info.plist UTI declarations. The current Info.plist has only the bare-minimum keys for Launchpad/Spotlight; if Seth ever wants drag-a-`.glabels`-onto-the-icon behavior, that's a separate Info.plist change. + +## Assumptions Made + +- The user has macOS 13+ (so `sips` handles SVG). Older macOS gets the rescue path with a generic icon — works, just ugly. The project audience (technical users with brew) skews to recent macOS, so this is acceptable. +- Brew's installed Qt6 binary correctly resolves Qt plugins via rpath when launched from a non-shell context (Launchpad). Standard for brew-installed Qt apps; unverified here. +- The `/Applications/` copy is not a deal-breaker for the user (vs. an automatic symlink). Users running brew already accept similar manual steps for `brew linkapps`-style integrations. +- Upstream's scalable SVG icon will continue to be installed at the spec'd path on macOS. If upstream restructures the icon install rules, the formula's glob may miss — `opoo` then falls through to generic icon (not a fail-build). + +## Potential Gotchas + +- **`brew --prefix glabels-qt`** in the caveats text resolves the formula's installed prefix. If the user runs the `cp -R` BEFORE actually installing, it'll error with "no formula found." The caveats text shows this command in the post-install message, which is where it makes sense. +- **Re-running `cp -R` after `brew upgrade`** is required because Launchpad caches the .app. The README documents this. Future improvement could be a small post-install hint via `brew services`-style integration, but that's out of scope. +- **Editing the formula requires care around heredoc delimiters.** The current file has three: `SH` (launcher), `PLIST` (Info.plist), `CAVEATS`. Each must open and close correctly; an unclosed heredoc would silently consume the rest of the file as content. Visual inspection on the live tap confirmed all three are balanced. +- **The .app wrapper's launcher script is shell, not a Mach-O binary.** This may cause Gatekeeper to flag it more aggressively on first launch than a signed Mach-O would. If it becomes a problem, alternatives are: (a) hardlink/symlink the real binary into the launcher path, (b) compile a tiny C program that `exec`s the real binary. + +## Environment State + +### Tools/Services Used + +- **Homebrew tap** at `git.sethpc.xyz/Seth/homebrew-tap` — separate Gitea repo (NOT inside sethLabels). Has its own per-release commit history (one commit per release, bumping `tag:` and `revision:`). +- **gitea CLI** (`~/bin/gitea`) — not directly used this session (the tap was already created in Task 11 of the previous session). +- **detect-secrets** (pre-commit hook on the tap repo) — flagged the revision SHA; resolved with inline pragma comment. + +### Active Processes + +- None. + +### Environment Variables + +- `HOMEBREW_PREFIX` — referenced in the formula's `caveats` text, resolves on the user's Mac. +- No new env vars set or required this session. + +## Related Resources + +- **First-release handoff (predecessor):** [`2026-04-29-155439-first-release.md`](./2026-04-29-155439-first-release.md) — predecessor capturing the 12-task implementation + first published release +- **Spec-approved-pre-implementation handoff (grandfather):** [`2026-04-29-095534-spec-approved-pre-implementation.md`](./2026-04-29-095534-spec-approved-pre-implementation.md) — the design state we resumed from +- **Design spec:** [`../sethlabels-docs/specs/2026-04-29-packaging-design.md`](../../sethlabels-docs/specs/2026-04-29-packaging-design.md) §D2 (brew tap decision) +- **Implementation plan:** [`../sethlabels-docs/plans/2026-04-29-packaging-implementation.md`](../../sethlabels-docs/plans/2026-04-29-packaging-implementation.md) — the 12-task plan that built the pipeline +- **Brew tap (live):** https://git.sethpc.xyz/Seth/homebrew-tap +- **First sethLabels release:** https://git.sethpc.xyz/Seth/sethLabels/releases/tag/3.99-master618-seth1 +- **Upstream glabels-qt cmake target (read-only reference):** `glabels/CMakeLists.txt:125` (the `WIN32` keyword without `MACOSX_BUNDLE`) and `glabels/CMakeLists.txt:152-156` (icon install rules) + +--- + +**Security Reminder**: No secrets present. Validated post-write.