Skip to content

release: ship v0.1.1 patch (Homebrew Formula icon sandbox fix) - #17

Merged
mitchelljphayes merged 2 commits into
mainfrom
release-v0.1.1
Sep 11, 2026
Merged

mitchelljphayes merged 2 commits into
mainfrom
release-v0.1.1

Conversation

@mitchelljphayes

Copy link
Copy Markdown
Owner

Summary

Patch the v0.1.0 Homebrew Formula so brew install --build-from-source switcheroo and brew test switcheroo succeed under Homebrew's build sandbox.

This is a patch release (0.1.0 → 0.1.1). No daemon, config, service-definition, or remapping changes. No new functionality.

Sandbox bug (root cause)

iconutil -c icns cannot run inside Homebrew's build sandbox. Homebrew's seatbelt profile denies a mach-lookup that iconutil requires to load its IconServices framework — the call fails with "Invalid Iconset" even when the input iconset is perfectly valid and the same exact command succeeds a millisecond later outside the sandbox.

The v0.1.0 Formula generated the .icns at install time via sips + iconutil + Dir.mktmpdir. That worked on a developer machine, failed under brew install --build-from-source.

A second, smaller issue: Formula#system's Sorbet sig does not accept out: / err: keyword arguments, so the out: File::NULL, err: File::NULL calls raised TypeError: Expected type T.any(Integer, Pathname, String, Symbol), got type Hash under HOMEBREW_SORBET_RUNTIME=1 (set by brew test).

A third: the published v0.1.0 tap uses name macos: "homebrew.mxcl.switcheroo" for the launchd Label, so the Formula template must use the name macos: form too.

Fix (static asset copy)

Replace runtime .icns generation with a pre-built bundle/AppIcon.icns copied at install time:

  • bundle/AppIcon.icns is now a tracked binary asset (generated outside the sandbox from bundle/AppIcon-1024.png using the 10 standard iconutil-recognized sizes: 16, 32, 128, 256, 512 + @2x each — not the 12-file set that includes the non-standard icon_64x64 pair iconutil silently ignores).
  • Formula install block now: cp icns_source, app/"Contents/Resources/AppIcon.icns" — no sips, no iconutil, no Dir.mktmpdir, no out:/err: kwargs.
  • scripts/generate_icns.sh regenerates the asset from the master PNG and validates output (ICNS magic, declared length, chunk-length sum, round-trip decode via iconutil -c iconset).
  • scripts/package.sh now prefers the tracked icns and only falls back to runtime generation if missing — single source of truth between packaging and Homebrew.
  • service do block gets name macos: "homebrew.mxcl.switcheroo".

Evidence (real, full, sandboxed)

Validated end-to-end on macOS 14.6 (Apple Silicon), Homebrew 4.x, against the actual v0.1.0 Formula + the new scripts:

  1. Brew test (full sandboxed)

    • brew install --build-from-source ./packaging/homebrew/switcheroo.rb.tpl (with __VERSION__=0.1.1, __SHA256__ of a local source tarball, __URL__ pointing at the local tarball).
    • brew test switcheroo ran the Formula's test block (switcheroo --version → switcheroo 0.1.1).
    • brew info switcheroo confirms the installed icon (Contents/Resources/AppIcon.icns) matches the source byte-for-byte:
      sha256: 42806c41405d55032548aa34493aeedd5493f59048b9f6792cb54942361c3106 (739 049 bytes, ic12).
    • No brew services start switcheroo was issued. No daemon was started.
  2. Sandbox-only regression test (scripts/test_icon_regression.sh)

    • Positive: invokes Homebrew's Sandbox API (not allow-default) and runs /bin/cp <icns> <dest> — the exact Formula icon-install code path. Verifies installed bytes match source (sha256 + size).
    • Negative control: same Sandbox runs /usr/bin/iconutil -c icns — confirms iconutil still fails under the current sandbox profile (proving the bug isn't papered over by some unrelated change).
    • Service Label: evaluates the template via brew ruby and asserts svc.plist_name == "homebrew.mxcl.switcheroo" and svc.plist_names == ["homebrew.mxcl.switcheroo"] (the latter fails if name macos: is regressed to positional name "...").
    • All three sub-tests pass.
  3. ICNS integrity (scripts/generate_icns.sh validation)

    • Magic icns ✓, declared length == actual ✓, chunk-length sum == actual ✓, round-trip iconutil -c iconset extracts exactly 10 PNGs ✓.

Files changed

  • Cargo.toml, Cargo.lock — version 0.1.0 → 0.1.1
  • bundle/Info.plist — CFBundleVersion, CFBundleShortVersionString → 0.1.1
  • bundle/AppIcon.icns — new tracked asset (739 049 bytes, ic12)
  • scripts/generate_icns.sh — new (regenerate + validate)
  • scripts/test_icon_regression.sh — new (sandbox regression)
  • scripts/package.sh — prefer tracked icns, fallback to runtime
  • packaging/homebrew/switcheroo.rb.tpl — copy pre-built icns, drop out:/err:, add name macos: in service do
  • CHANGELOG.md — ## [0.1.1] - 2026-09-12 Fixed block
  • .github/workflows/ci.yml — register new scripts in code-hygiene, add icon-regression job

Out of scope

  • Tap (homebrew-core or personal tap) update — handled separately after the GitHub release.
  • No binary assets attached to the GitHub release — source-only, same as v0.1.0.
  • Raycast extension, README, RELEASING.md, other docs — untouched.

Checklist

  • cargo build (release) passes locally
  • cargo test passes locally
  • brew install --build-from-source + brew test passes in real sandbox
  • ICNS asset present and matches master PNG round-trip
  • CHANGELOG wording matches actual DSL (name macos: "...")
  • No daemon started during validation
  • Version surfaces (Cargo.toml, Cargo.lock, Info.plist, CHANGELOG) all 0.1.1

Patch the v0.1.0 Homebrew Formula under the real build sandbox:

- Icon install: drop runtime sips/iconutil/Dir.mktmpdir. `iconutil -c
  icns` cannot run inside Homebrew's seatbelt sandbox (denies the
  mach-lookup iconutil needs, so even valid iconsets come out
  'Invalid Iconset'). Copy a pre-built bundle/AppIcon.icns instead.
- Drop `out: File::NULL, err: File::NULL` from Formula `system` calls
  — Formula#system's Sorbet sig does not accept these kwargs and
  raised TypeError under HOMEBREW_SORBET_RUNTIME=1 (e.g. brew test).
- Add explicit `name macos: "homebrew.mxcl.switcheroo"` to the
  service do block so the published tap matches the upstream Label.

Assets + tooling:
- Track bundle/AppIcon.icns (the same file the Formula copies).
- scripts/generate_icns.sh: regenerate bundle/AppIcon.icns from the
  1024x1024 master PNG outside the sandbox (10 standard iconutil
  sizes; validates magic + declared length + round-trip decode).
- scripts/test_icon_regression.sh: regression test that runs the
  exact Formula icon-install code under Homebrew's Sandbox API and
  confirms iconutil still fails there (negative control). Also
  validates the service plist_name from the template.
- scripts/package.sh: prefer the tracked icns, fall back to runtime
  generation only if missing.
- ci.yml: register new scripts in code-hygiene, add icon-regression
  job on macos-14.

Version surfaces bumped to 0.1.1: Cargo.toml, Cargo.lock, Info.plist,
CHANGELOG.md. No daemon, config, service, or mapping changes. No
binary assets attached to the GitHub release (source-only).
The previous version of scripts/test_icon_regression.sh asserted
`svc.plist_names == [expected_label]` to catch a regression to the
positional `name "..."` form. `Homebrew::Service#plist_names`
is not available on the Homebrew version installed on the macos-14
runner (NoMethodError), so the assertion failed even though
`svc.plist_name == "homebrew.mxcl.switcheroo"` (the canonical
homebrew.mxcl.<name> Label) passed.

Narrow the test to:
  - source-level guard: the template must contain the explicit
    `name macos:` line (catches regression to positional
    `name "..."`, which raises ArgumentError at install time).
  - Service model guard: `svc.plist_name == "homebrew.mxcl.switcheroo"`.

The end-to-end `name "..."` regression is also covered by the
documented `brew install --build-from-source` + `brew test` run
on the v0.1.1 PR description.
@mitchelljphayes
mitchelljphayes merged commit 5cc4437 into main Sep 11, 2026
12 checks passed
@mitchelljphayes
mitchelljphayes deleted the release-v0.1.1 branch September 11, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant