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)
f6c30f2test: move bats scratch dirs to repo-local .test-scratch/ (per global no-/tmp/ rule)76a3cb7docs: README.sethlabels.md — include upstream remote setup in build-from-source891cc7cdocs: add session handoff (first-release)2d04943docs: refresh CLAUDE.md to post-first-release phase2108e2cdocs: 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
- 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 <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_BUNDLEthe cmake target (needs upstream patch — strict-zero forbids) - Running
macdeployqton 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 (indef install) and the user message (indef caveats). On Linux brew (rare for this formula), the .app code is skipped entirely.- Path interpolation in heredocs — the launcher script uses
#{bin}/glabels-qtinterpolation. 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. sipsfor SVG → icns — macOS-built-in tool. Wrapped inbegin/rescueso a sips failure on older macOS (which can't read SVG) justopoos 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 secretinline comment. Every future tap bump must preserve that pragma. Future improvement: add the SHA pattern to.secrets.baselineso the pragma isn't needed.
Work Completed
Tasks Finished
- Diagnosed the menu-launcher question by reading upstream
glabels/CMakeLists.txt:125,150(noMACOSX_BUNDLE) anddocs/BUILD-INSTRUCTIONS-MACOS.md(CLI-onlymake 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+caveatsmethod + README.md "Launchpad / Spotlight" section. Committed asef4d6c7on the tap. - Caught a bug via context check: the initial implementation globbed for
glabels.pngbut upstream installs only SVGs (glabels/CMakeLists.txt:152-156). Theopoofallback 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, wrapsipsinbegin/rescue. Committed as3542762on 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
- 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-qtfinds the binary,cp -R "$(brew --prefix glabels-qt)/glabels-qt.app" /Applications/succeeds, the app appears in Launchpad and is searchable in Spotlight. Ifsipsfailed silently (older macOS), the icon will be generic — check Launchpad first. - 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.
- 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 ./..., runglabels-qt --version. If unmet deps surface, override viaCPACK_DEBIAN_PACKAGE_DEPENDSinscripts/build-deb.shand re-tag as -seth2.
Blockers / Open Questions
- Does
sipson the user's macOS version handle SVG input? macOS 13+ should; older versions may fail. Therescueblock 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.baselinein the tap repo — would eliminate the need for# pragma: allowlist secreton every future tap bump. Trivial change but I left it for a future session since it's not blocking. - Verifying the
.appactually contains all needed Qt6 plugins on user-side — brew's Qt6 ships with cocoa platform plugin, image format plugins, and SVG support. The wrapper'sexecof 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.envis currently unwired,compute-version.shopaque 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
sipshandles 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 forbrew 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 —
opoothen falls through to generic icon (not a fail-build).
Potential Gotchas
brew --prefix glabels-qtin the caveats text resolves the formula's installed prefix. If the user runs thecp -RBEFORE 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 -Rafterbrew upgradeis required because Launchpad caches the .app. The README documents this. Future improvement could be a small post-install hint viabrew 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, bumpingtag:andrevision:). - 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'scaveatstext, 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— predecessor capturing the 12-task implementation + first published release - Spec-approved-pre-implementation handoff (grandfather):
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§D2 (brew tap decision) - Implementation plan:
../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(theWIN32keyword withoutMACOSX_BUNDLE) andglabels/CMakeLists.txt:152-156(icon install rules)
Security Reminder: No secrets present. Validated post-write.