feat(macos): migrate SDRMac app from SwiftPM to Xcode project - #294
Conversation
Prep for the M4 Metal renderer and M6 signing/notarization. Both benefit from Xcode's native support: - `.metal → default.metallib` compiles at build time (was a runtime `makeLibrary(source:)` pass before) - Proper `.app` bundles out of xcodebuild (was a hand-rolled `bundle-mac-app.sh` wrapping the SwiftPM Mach-O) - Metal frame capture / shader debugger / Instruments integration work natively - M6 code-signing + notarization fits into xcodebuild's existing pipeline ## What changed - New `apps/macos/SDRMac.xcodeproj/` — Xcode 15+ pbxproj using `PBXFileSystemSynchronizedRootGroup` so file management stays out of the project file (groups auto-pick up files in their target directories; no per-file UUID bookkeeping). - `SdrCoreKit` stays a SwiftPM package; Xcode references it via `XCLocalSwiftPackageReference` at `Packages/SdrCoreKit`. No change to SdrCoreKit itself beyond a doc comment update. - `make mac-app` / `mac-app-debug` now shell out to xcodebuild instead of `swift build` + `bundle-mac-app.sh`. New `make mac-test` runs the XCTest suite via xcodebuild. - Target naming split: Xcode target and Swift module name stay `SDRMac` (Swift forbids hyphens in module identifiers). `PRODUCT_NAME = sdr-rs` and `PRODUCT_MODULE_NAME = SDRMac` get us `sdr-rs.app` output with `import SDRMac` preserved. - `AppIcon.icns` is now committed under `SDRMac/Resources/AppIcon.icns` — the Xcode synchronized-root-group picks it up as a resource automatically. Regenerate from the SVG via `./scripts/make-app-icon.sh SDRMac/Resources` when `data/com.sdr.rs.svg` changes. ## Dropped - `apps/macos/Package.swift` — the SwiftPM-parent that used to build the app. Replaced by the Xcode project. - `apps/macos/scripts/bundle-mac-app.sh` — the hand-rolled `.app` wrapper / ad-hoc-sign script. xcodebuild does both natively. ## Security pre-flight on the new pbxproj User asked. Scanned and verified: - No `DEVELOPMENT_TEAM` (Xcode's template had the team ID the user signed in with; stripped it, `CODE_SIGN_STYLE = Automatic` re-derives locally on open) - No provisioning profile identifiers - No personal email / name - No credentials - `xcuserdata/` under both the .xcodeproj and inner .xcworkspace is .gitignored — contains breakpoints, UI state, team cache, all per-machine local. ## Build-setting deltas vs Xcode template - `MACOSX_DEPLOYMENT_TARGET = 14.0` (was 26.4) - `SDKROOT = macosx`, `SUPPORTED_PLATFORMS = macosx` (was auto + iphoneos/iphonesimulator/xros/xrsimulator — we're macOS-only) - `PRODUCT_NAME = sdr-rs` + `PRODUCT_MODULE_NAME = SDRMac` - `PRODUCT_BUNDLE_IDENTIFIER = com.sdr.rs` (matches `com.sdr.rs.desktop` already used by the Linux side) - `GENERATE_INFOPLIST_FILE = NO` + `INFOPLIST_FILE = SDRMac/ Resources/Info.plist` (keep the plist we hand-maintained) - `CODE_SIGN_ENTITLEMENTS = SDRMac/Entitlements/SDRMac. entitlements` - `ENABLE_APP_SANDBOX = NO` (non-sandbox for USB access per the epic spec) - Dropped the UI test target — we don't use it and the template generated it by default ## Tests - `make mac-test` runs 10 tests via xcodebuild: 3 CoreModel smoke + 7 CoreModelIntegration end-to-end against a real SdrCore - `make swift-test` (SdrCoreKit's own FFI integration tests) still runs via SwiftPM as before Also: - `CoreModelTests.swift` needed an explicit `import SdrCoreKit` because `SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES` enforces explicit imports for re-exported types (`.wfm` DemodMode etc.). CoreModelIntegrationTests already had this import from before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughMigrates macOS app from a SwiftPM-root layout to an Xcode-driven workflow: removes Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/macos/SDRMac.xcodeproj/project.pbxproj`:
- Around line 6-54: The project.pbxproj uses Xcode-16-only settings:
objectVersion = 77 and ISA types PBXFileSystemSynchronizedRootGroup /
PBXFileSystemSynchronizedBuildFileExceptionSet which break Xcode 15; change
objectVersion to 63 and replace the PBXFileSystemSynchronized* entries (e.g.,
7CFEC5F02F91BB3000EC173F, 7CFEC5C72F91BB2900EC173F, 7CFEC5D52F91BB2A00EC173F)
with standard PBXGroup/PBXGroup-like entries (move membershipExceptions into
normal group file lists or a PBXGroup child) so the project parses on Xcode 15,
or alternatively update the repo docs to require Xcode 16 minimum if you prefer
to keep objectVersion = 77 and the synchronized group ISAs.
In `@Makefile`:
- Around line 387-399: The mac-test Makefile target runs xcodebuild without a
preflight check; add the same guard used in mac-app/mac-app-debug to verify
XCODEBUILD is available before running xcodebuild and exit early with a friendly
message if not present. Update the mac-test target (referencing target name
"mac-test" and the variable "XCODEBUILD") to perform a command existence check
(e.g., command -v $(XCODEBUILD) or test -x) and print "mac-test: xcodebuild not
found; skipping" (or similar) and exit 0 when missing, then proceed to the
existing cargo build and xcodebuild steps if the check passes.
- Around line 349-358: The Makefile currently masks xcodebuild failures by
piping its output to grep and using "|| true", which causes the script to
proceed and copy a potentially stale app; change the mac-app-release (and
mac-app-debug) build steps to capture xcodebuild output into a temporary file,
test the xcodebuild exit status explicitly (using $(XCODEBUILD) return code)
before doing the cp -R from $(SDR_MAC_DD), and only copy the built sdr-rs.app
when xcodebuild succeeded—follow the same pattern used in the ffi-header-check
target (use a temp logfile, check command exit code, log errors and abort if
nonzero) and reference the XCODEBUILD invocation,
$(SDR_MAC_DD)/Build/Products/Release/sdr-rs.app, and the cp -R step when
applying the fix.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f20742a4-db33-410b-a70b-5522a5b6fe9a
📒 Files selected for processing (10)
.gitignoreMakefileapps/macos/Package.swiftapps/macos/Packages/SdrCoreKit/Package.swiftapps/macos/README.mdapps/macos/SDRMac.xcodeproj/project.pbxprojapps/macos/SDRMac.xcodeproj/project.xcworkspace/contents.xcworkspacedataapps/macos/SDRMac/Resources/AppIcon.icnsapps/macos/SDRMacTests/CoreModelTests.swiftapps/macos/scripts/bundle-mac-app.sh
💤 Files with no reviewable changes (2)
- apps/macos/scripts/bundle-mac-app.sh
- apps/macos/Package.swift
## Makefile: surface xcodebuild failures (Major) `mac-app` / `mac-app-debug` previously piped xcodebuild's output through `grep ... || true`. The `|| true` catches grep's exit code, not xcodebuild's, so a silent xcodebuild failure (missing SDK, signing error, malformed pbxproj) would fall through to the `cp -R` step — which would copy a STALE `.app` from a previous successful build and `make mac-app` would claim success. Fixed: capture xcodebuild output to a temp file, check its real exit status with `if ! xcodebuild ...`, dump the log and `exit 1` on failure. On success, filter the log for the `error:` / `warning:` / `** XX **` lines we care about displaying. Same pattern as `ffi-header-check`. ## Makefile: mac-test missing xcodebuild preflight (Minor) `mac-test` was missing the `command -v $(XCODEBUILD)` guard that `mac-app` and `mac-app-debug` both have. Added the same friendly error + `exit 1` path so running on a mac without Xcode installed fails with a clear message instead of a cryptic command-not-found. ## Xcode 16 minimum requirement documented (Major — docs) The pbxproj uses `objectVersion = 77` and the `PBXFileSystemSynchronizedRootGroup` / `PBXFileSystemSynchronizedBuildFileExceptionSet` ISAs that Xcode 16 added. Xcode 15 and earlier can't parse these. The synchronized-groups format is worth keeping — it means source files auto-sync from the filesystem without per-file UUID bookkeeping in the pbxproj (critical for a project that'll grow fast as M4/M5 land). Documented the Xcode 16 minimum in `apps/macos/README.md` rather than downgrading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First checkpoint of the M4 Metal renderer. Gets a real `MTKView` compiling and drawing inside the SwiftUI app, fed by a synthetic FFT source so the pipeline can be validated in isolation from the engine. Sub-PRs 2+ add the waterfall ring, real `SdrCore` FFT pull, VFO overlay, and the frequency-scale overlay. ## What lands - `Renderer/Shaders.metal`: spectrum line vertex+fragment. SDR-green accent color modulated slightly by intensity so peaks read brighter than the noise floor. Same `Uniforms` struct the waterfall / VFO shaders will reuse. - `Renderer/SpectrumMTKView.swift`: MTKView subclass owning the command queue, pipeline state, and a pre-allocated vertex buffer sized for the max FFT (8192 bins → 32 KB). `storageModeShared` so per-frame CPU writes are visible to the GPU without a blit. Factory pattern (`make() -> Self?`) for fail-soft Metal setup since MTKView's designated init isn't failable. - `Renderer/SpectrumWaterfallView.swift`: NSViewRepresentable bridge. Falls back to a SwiftUI-hosted NSTextField label if Metal setup fails (e.g., Macs predating our macOS 14 floor — unlikely but cheap to handle). - `Renderer/Palettes.swift`: turbo LUT, 256×1 rgba8Unorm, used by the waterfall shader in sub-PR 2. Precomputed from the Google polynomial at compile time. - `Renderer/SyntheticFftSource.swift`: deterministic pseudo-noise floor with three Gaussian peaks drifting at different speeds. Enough motion to prove the GPU path works without needing a dongle. Sub-PR 3 removes this. - `Views/CenterView.swift`: replaces the placeholder (wavefom icon + "Metal renderer lands in M4" text) with the real `SpectrumWaterfallView`, wired to `minDb` / `maxDb` bindings on `CoreModel`. ## Power discipline - `enableSetNeedsDisplay = true` + `isPaused = true`. We draw only when explicitly invalidated. A 20 Hz `Timer` on `RunLoop.main` advances the synthetic source and calls `needsDisplay = true` — matching the engine's default FFT rate. ~3× GPU power savings vs a free-running 60 Hz vsync tick, which matters for a laptop/iPad form factor. - Sub-PR 3 adds `IOPSCopyPowerSourcesInfo`-based AC-vs- battery detection and flips to continuous vsync on plug-in (smooth ProMotion when power isn't a concern). ## Zero-allocation path Per-frame work is memcpy + one encoder setup + `drawPrimitives`. No Swift heap allocations. Vertex buffer + pipeline state + Metal device live for the lifetime of the view. ## Visually verified Three moving peaks on a noise floor, smooth motion at 20 Hz, Min/Max dB sliders in the Display sidebar correctly rescale the vertical axis. Bundle ships `default.metallib` next to the Mach-O (compiled by Xcode's build pipeline — part of the win from the PR #294 migration). ## Follow-ups in this branch - Sub-PR 2: waterfall texture ring + scroll shader, viewport split, palette LUT tied in - Sub-PR 3: swap `SyntheticFftSource` for `SdrCore.withLatestFftFrame`; add `PowerMode` IOPS observer Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Moves the
SDRMacapp from a hand-rolled SwiftPM +bundle-mac-app.shdev loop to a real Xcode project. Prepfor the M4 Metal renderer (next PR) and M6 signing /
notarization (later).
.metal→default.metallibat build time instead of aruntime
makeLibrary(source:)pass.appbundles direct fromxcodebuildinstead ofwrapping a bare Mach-O
integration
pipeline without reinventing the wrapper
What changed
apps/macos/SDRMac.xcodeproj/using Xcode 15+PBXFileSystemSynchronizedRootGroup— source files areauto-picked from the directory, no per-file UUID bookkeeping
SdrCoreKitstays a SwiftPM package, referenced from Xcodevia
XCLocalSwiftPackageReferencemake mac-app/mac-app-debugnow shell out toxcodebuild. Newmake mac-testruns XCTest via xcodebuildSDRMac(Swift forbids hyphens inidentifiers; keeps
import SDRMacworking)PRODUCT_NAME = sdr-rs— output issdr-rs.appPRODUCT_BUNDLE_IDENTIFIER = com.sdr.rs(matches Linuxcom.sdr.rs.desktop)AppIcon.icnsnow committed underSDRMac/Resources/;regenerate via
./scripts/make-app-icon.sh SDRMac/Resourceswhen
data/com.sdr.rs.svgchangesDropped
apps/macos/Package.swift— the parent SwiftPM that builtthe app
apps/macos/scripts/bundle-mac-app.sh— the hand-rolledwrapper / ad-hoc-sign script (xcodebuild does both natively)
Security pre-flight
User asked specifically. Scanned the generated pbxproj and
stripped:
DEVELOPMENT_TEAM(Xcode's template embeds the local AppleID; gone —
CODE_SIGN_STYLE = Automaticre-derives on open)iphonesimulator / xros / xrsimulator, we're macOS-only)
Also
.gitignore'dxcuserdata/at both the.xcodeprojandinner
.xcworkspacelevels — contains breakpoints, UI layout,team cache, all machine-local.
Build-setting deltas vs Xcode template
MACOSX_DEPLOYMENT_TARGET = 14.0(was 26.4)SDKROOT = macosx,SUPPORTED_PLATFORMS = macosxPRODUCT_NAME = sdr-rs,PRODUCT_MODULE_NAME = SDRMacPRODUCT_BUNDLE_IDENTIFIER = com.sdr.rsGENERATE_INFOPLIST_FILE = NO+INFOPLIST_FILE = SDRMac/Resources/Info.plistCODE_SIGN_ENTITLEMENTS = SDRMac/Entitlements/SDRMac.entitlementsENABLE_APP_SANDBOX = NO(non-sandbox for USB per epic spec)Test plan
cargo fmt --all -- --check,cargo clippy --all-targets --workspace -- -D warnings,cargo test --workspace(17 test suites green)make mac-app→ producesapps/macos/build/sdr-rs.appopen apps/macos/build/sdr-rs.app— app launches,window renders, Source/Radio/Display panels visible,
toolbar works
audio on 89.1 FM (release build only, per prior
findings)
make mac-test— 10 tests pass via xcodebuildmake swift-test— SdrCoreKit's own 12 FFIintegration tests pass
Next
M4 Metal renderer (spectrum + waterfall) lands in a follow-up
PR now that the Xcode project can host
.metalshaders andtie them into the build at compile time.
🤖 Generated with Claude Code
Summary by CodeRabbit