904b9b3d-c0eb-42f3-acef-958.../build.sh
徐翔宇 d5934b06b0 v2.1.0: Atoll-style MediaRemoteAdapter — bypass 15.4+ entitlement gate
Ports the MediaRemoteAdapter pattern from Atoll
(github.com/Ebullioscopic/Atoll). On macOS 15.4+, Apple gated
MRMediaRemoteGetNowPlayingInfo behind a private entitlement, which made
our previous MediaRemoteSource return empty dicts and forced us onto
slow-path AppleScript polling. This commit bundles Jonas van den Berg's
MediaRemoteAdapter.framework (BSD-3-Clause) plus mediaremote-adapter.pl
and runs them as a subprocess — the framework links against Apple's MR
in a way that skips the caller-side entitlement check, so we get the
full now-playing payload (title, artist, album, duration, elapsed,
isPlaying, artwork, bundleIdentifier) pushed to us in real time.

Bundle additions (~500KB total):
- Resources/MediaRemoteAdapter.framework (universal x86_64 + arm64 + arm64e)
- Resources/mediaremote-adapter.pl
- LICENSE-THIRD-PARTY.md with full BSD-3-Clause attribution

New source: MediaRemoteAdapterSource.swift
- Spawns /usr/bin/perl with minimal env (PATH + LANG only).
- FileHandle.readabilityHandler ingests newline-delimited JSON stream
  from stdout, parses via Codable AdapterStreamPayload, merges diffs
  into persistent MediaRemoteInfo so playbackRate-only payloads don't
  erase title/artist.
- Artwork base64 decoded via Data default strategy.
- Crash handling: SIGTERM → 500ms → SIGKILL on stop. Auto-restart with
  exponential backoff (1s/2s/4s), circuit-breaker after 3 crashes
  within 60s → fall back to legacy chain.
- Transport controls (togglePlay/next/prev/seek) via short-lived one-shot
  `perl adapter.pl send N` subprocesses. send codes: 2=toggle, 4=next,
  5=prev. seek takes microseconds.

NowPlayingState wiring:
- New sticky kind `.mediaRemoteAdapter`, highest priority.
- `applyAdapterUpdate(_:)` publishes directly (no router pass).
- `routeSources` short-circuits when adapter is sticky + has data —
  subprocess pushes fresh data on every change, polling would be pure
  waste.
- `adaptivePollInterval()` returns 30s for adapter (safety net only).
- `isCandidateLive` + `tryFetch` treat adapter as push-only (returns nil
  from pull-fetch so the sticky fast-path falls through to parallel
  probing if subprocess is dead).
- `stop()` terminates the subprocess cleanly.
- Transport controls route to adapter.sendCommand() / adapter.seek()
  when it's the sticky source.

Build:
- build.sh copies Resources/ into Contents/Resources with preserved
  exec bits on the framework binary + Perl script.
- `codesign --force --deep --sign -` re-signs the whole tree ad-hoc
  so the nested framework inherits our identity and Gatekeeper loads
  it without complaint.
- Bundle grew from 48KB → 1.6MB (zipped 564KB). Acceptable for the
  latency win: Apple Music track switches now visible <100ms vs prior
  800ms adaptive-poll worst case.

Security audit (done before bundling):
- Perl script: strict + warnings, whitelisted function names, no
  shell-out, no network I/O, params passed to framework via ENV
  (no string concat). Safe.
- Framework: ad-hoc signed (Identifier com.vandenbe.MediaRemoteAdapter).
  --deep re-sign with our identity replaces the original ad-hoc cert so
  signature validation passes locally and in Gatekeeper.
- Subprocess runs with PATH=/usr/bin:/bin + LANG only. No inherited
  secrets.
- Explicit Process arguments array — no shell interpolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:19:40 +08:00

65 lines
2.4 KiB
Bash
Executable File

#!/bin/bash
# Build the Music Player plugin as a .bundle for MioIsland
set -e
PLUGIN_NAME="music-player"
BUNDLE_NAME="${PLUGIN_NAME}.bundle"
BUILD_DIR="build"
# Recursively pick up every .swift under Sources/ (root + subdirectories
# like sources/, ui/, support/ for the v2.0.0 layered layout).
SOURCES=$(find Sources -name "*.swift" -type f)
echo "Building ${PLUGIN_NAME} plugin..."
echo "Compiling $(echo "$SOURCES" | wc -l | tr -d ' ') Swift files..."
# Clean
rm -rf "${BUILD_DIR}"
mkdir -p "${BUILD_DIR}/${BUNDLE_NAME}/Contents/MacOS"
# Compile to dynamic library
swiftc \
-emit-library \
-module-name MusicPlugin \
-target arm64-apple-macos15.0 \
-sdk $(xcrun --show-sdk-path) \
-o "${BUILD_DIR}/${BUNDLE_NAME}/Contents/MacOS/MusicPlugin" \
${SOURCES}
# Copy Info.plist
cp Info.plist "${BUILD_DIR}/${BUNDLE_NAME}/Contents/"
# Bundle the MediaRemoteAdapter subprocess payload (Atoll-style).
# Resources/ contains `MediaRemoteAdapter.framework` + `mediaremote-adapter.pl`.
# Both are BSD-3-Clause by Jonas van den Berg (see LICENSE-THIRD-PARTY.md).
# We copy Resources/* into Contents/Resources so MediaRemoteAdapterSource
# can find them via Bundle(for:).path(forResource:ofType:).
if [ -d "Resources" ]; then
mkdir -p "${BUILD_DIR}/${BUNDLE_NAME}/Contents/Resources"
cp -R Resources/* "${BUILD_DIR}/${BUNDLE_NAME}/Contents/Resources/"
# Preserve framework executable bit (cp -R should, but be defensive)
chmod +x "${BUILD_DIR}/${BUNDLE_NAME}/Contents/Resources/MediaRemoteAdapter.framework/MediaRemoteAdapter" 2>/dev/null || true
chmod +x "${BUILD_DIR}/${BUNDLE_NAME}/Contents/Resources/mediaremote-adapter.pl"
fi
# Ad-hoc sign the WHOLE bundle including the nested framework. Passing
# --deep traverses nested code signatures and re-signs them with our
# ad-hoc identity so the framework loads without Gatekeeper complaints
# when the plugin is dropped into ~/.config/codeisland/plugins/.
codesign --force --deep --sign - "${BUILD_DIR}/${BUNDLE_NAME}"
echo "✓ Built ${BUILD_DIR}/${BUNDLE_NAME}"
# Create zip for marketplace upload
cd "${BUILD_DIR}"
zip -r "${PLUGIN_NAME}.zip" "${BUNDLE_NAME}"
cd ..
echo "✓ Created ${BUILD_DIR}/${PLUGIN_NAME}.zip (for marketplace upload)"
echo ""
echo "Install locally:"
echo " cp -r ${BUILD_DIR}/${BUNDLE_NAME} ~/.config/codeisland/plugins/"
echo ""
echo "Upload to marketplace:"
echo " ${BUILD_DIR}/${PLUGIN_NAME}.zip"