Files
sethLabels/.claude/handoffs/2026-04-29-121529-mac-app-launcher.md

18 KiB

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

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 <prefix>/glabels-qt.app/Contents/{Info.plist, MacOS/glabels-qt, Resources/glabels-qt.icns} where the launcher script is a 2-line shell that execs 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/<version>/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 opoos 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

  • 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)
  • Presented three implementation options to Seth; he picked option 1 (stub .app via brew formula)
  • Implementer subagent generated the .app wrapper block in def install + caveats method + README.md "Launchpad / Spotlight" section. Committed as ef4d6c7 on the tap.
  • 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.
  • 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.
  • 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).
  • 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 execs 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.

Security Reminder: No secrets present. Validated post-write.