diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml index c70ea68..92aba47 100644 --- a/.github/workflows/finalize-release.yml +++ b/.github/workflows/finalize-release.yml @@ -57,14 +57,21 @@ jobs: test "$(brew info --json=v2 DevVig/microbridge/microbridge | jq -r '.formulae[0].installed[0].version')" = "$VERSION" microbridgectl help | grep -q Usage APP="$HOME/Applications/Microbridge.app" + MARKER="$HOME/Applications/.Microbridge.app.microbridge-brew" + LEGACY_MARKER="$APP/.microbridge-brew" test -x "$(brew --prefix DevVig/microbridge/microbridge)/bin/microbridged" brew services start DevVig/microbridge/microbridge for _ in {1..30}; do - [[ -f "$APP/.microbridge-brew" ]] && break + [[ -f "$MARKER" ]] && break sleep 1 done - test -f "$APP/.microbridge-brew" + test -f "$MARKER" + test ! -e "$LEGACY_MARKER" test "$(/usr/libexec/PlistBuddy -c 'Print:CFBundleShortVersionString' "$APP/Contents/Info.plist")" = "$VERSION" + codesign --verify --deep --strict --verbose=4 "$APP" + spctl --assess --type execute --verbose=4 "$APP" + xcrun stapler validate "$APP" + syspolicy_check distribution "$APP" SERVICE_STATE="" for _ in {1..30}; do SERVICE_STATE="$(brew services list | awk '$1 == "microbridge" { print $2; exit }')" @@ -92,9 +99,11 @@ jobs: echo "microbridge service is still registered after uninstall" >&2 exit 1 fi - test -f "$APP/.microbridge-brew" + test -f "$MARKER" rm -rf "$APP" + rm -f "$MARKER" test ! -e "$APP" + test ! -e "$MARKER" promote: name: promote verified release diff --git a/Formula/microbridge.rb b/Formula/microbridge.rb index 7bbe294..6f40b71 100644 --- a/Formula/microbridge.rb +++ b/Formula/microbridge.rb @@ -68,15 +68,18 @@ def install source_app="#{opt_prefix}/Microbridge.app" apps_dir="${HOME}/Applications" dest="${apps_dir}/Microbridge.app" - marker="${dest}/.microbridge-brew" + marker="${apps_dir}/.Microbridge.app.microbridge-brew" + legacy_marker="${dest}/.microbridge-brew" /bin/mkdir -p "${apps_dir}" - if [ -e "${dest}" ] && [ ! -f "${marker}" ]; then + if [ -e "${dest}" ] && [ ! -f "${marker}" ] && [ ! -f "${legacy_marker}" ]; then echo "Microbridge: preserving unowned ${dest}" >&2 else if [ -e "${dest}" ]; then /bin/rm -rf "${dest}" fi /usr/bin/ditto "${source_app}" "${dest}" + # Keep ownership state beside the signed bundle. Adding any file to + # Microbridge.app invalidates its sealed code signature. /usr/bin/touch "${marker}" fi exec "#{opt_bin}/microbridged" diff --git a/apps/microbridge-ui/src-tauri/src/lib.rs b/apps/microbridge-ui/src-tauri/src/lib.rs index 25a0e15..98ac11e 100644 --- a/apps/microbridge-ui/src-tauri/src/lib.rs +++ b/apps/microbridge-ui/src-tauri/src/lib.rs @@ -690,11 +690,12 @@ fn resize_popover(app: AppHandle, height: f64) { let _ = window.set_size(LogicalSize::new(POPOVER_WIDTH, clamped)); } -/// Install channel of the running app. Homebrew drops a `.microbridge-brew` -/// marker at the bundle root (installed by the formula's service wrapper); its absence -/// means a DMG/manual install. The in-app self-updater only replaces `direct` -/// installs — brew copies are routed to `brew upgrade` so the formula version -/// and the on-disk bundle never drift apart. +/// Install channel of the running app. Homebrew drops an ownership marker next +/// to the bundle (installed by the formula's service wrapper); its absence means +/// a DMG/manual install. The marker must stay outside `Microbridge.app`, because +/// adding a file to the bundle after signing invalidates its sealed signature. +/// The in-app self-updater only replaces `direct` installs — brew copies are +/// routed to `brew upgrade` so the formula version and bundle never drift apart. #[tauri::command] fn update_channel() -> String { if brew_marker_present() { @@ -704,15 +705,22 @@ fn update_channel() -> String { } } -/// True when the running bundle carries Homebrew's ownership marker. +/// True when the running bundle has Homebrew's ownership marker beside it. /// `…/Microbridge.app/Contents/MacOS/microbridge-ui` → the bundle root is the -/// third ancestor of the executable. +/// third ancestor of the executable. The in-bundle check is migration support +/// for installs made before the sidecar marker was introduced. fn brew_marker_present() -> bool { let Ok(exe) = std::env::current_exe() else { return false; }; match exe.ancestors().nth(3) { - Some(bundle) => bundle.join(".microbridge-brew").exists(), + Some(bundle) => { + let sidecar = bundle + .parent() + .map(|parent| parent.join(".Microbridge.app.microbridge-brew")); + sidecar.is_some_and(|marker| marker.exists()) + || bundle.join(".microbridge-brew").exists() + } None => false, } } diff --git a/scripts/smoke-formula.sh b/scripts/smoke-formula.sh index 79940c5..15433a4 100755 --- a/scripts/smoke-formula.sh +++ b/scripts/smoke-formula.sh @@ -10,14 +10,17 @@ VERSION="${2:?usage: $0 path/to/microbridge.rb VERSION ARCH}" EXPECTED_ARCH="${3:?usage: $0 path/to/microbridge.rb VERSION ARCH}" TAP="devvig/microbridge-ci" APP="$HOME/Applications/Microbridge.app" +MARKER="$HOME/Applications/.Microbridge.app.microbridge-brew" +LEGACY_MARKER="$APP/.microbridge-brew" BREW_LOG="$(brew --prefix)/var/log/microbridge.log" APP_LOG="${RUNNER_TEMP:-/tmp}/microbridge-app.log" cleanup() { brew services stop "$TAP/microbridge" >/dev/null 2>&1 || true HOMEBREW_NO_INSTALL_CLEANUP=1 brew uninstall "$TAP/microbridge" >/dev/null 2>&1 || true - if [[ -f "$APP/.microbridge-brew" ]]; then + if [[ -f "$MARKER" || -f "$LEGACY_MARKER" ]]; then rm -rf "$APP" + rm -f "$MARKER" fi brew untap "$TAP" >/dev/null 2>&1 || true } @@ -66,12 +69,17 @@ test -x "$PREFIX/bin/microbridgectl" brew services start "$TAP/microbridge" for _ in {1..30}; do - [[ -f "$APP/.microbridge-brew" ]] && break + [[ -f "$MARKER" ]] && break sleep 1 done test -d "$APP" -test -f "$APP/.microbridge-brew" +test -f "$MARKER" +test ! -e "$LEGACY_MARKER" test "$(/usr/libexec/PlistBuddy -c 'Print:CFBundleShortVersionString' "$APP/Contents/Info.plist")" = "$VERSION" +codesign --verify --deep --strict --verbose=4 "$APP" +spctl --assess --type execute --verbose=4 "$APP" +xcrun stapler validate "$APP" +syspolicy_check distribution "$APP" SERVICE_STATE="" for _ in {1..30}; do @@ -98,8 +106,10 @@ fi # The app is deliberately outside the Cellar so the menu-bar UI survives # formula upgrades. Remove it only after verifying this install's marker. -test -f "$APP/.microbridge-brew" +test -f "$MARKER" rm -rf "$APP" +rm -f "$MARKER" test ! -e "$APP" +test ! -e "$MARKER" brew untap "$TAP" trap - EXIT