Skip to content

feat(macos): migrate SDRMac app from SwiftPM to Xcode project - #294

Merged
jasonherald merged 2 commits into
mainfrom
feature/xcode-project-migration
Apr 17, 2026
Merged

feat(macos): migrate SDRMac app from SwiftPM to Xcode project#294
jasonherald merged 2 commits into
mainfrom
feature/xcode-project-migration

Conversation

@jasonherald

@jasonherald jasonherald commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Summary

Moves the SDRMac app from a hand-rolled SwiftPM +
bundle-mac-app.sh dev loop to a real Xcode project. Prep
for the M4 Metal renderer (next PR) and M6 signing /
notarization (later).

  • .metaldefault.metallib at build time instead of a
    runtime makeLibrary(source:) pass
  • Proper .app bundles direct from xcodebuild instead of
    wrapping a bare Mach-O
  • First-class Metal frame-capture / shader-debugger / Instruments
    integration
  • M6 code-signing + notarization fits the existing xcodebuild
    pipeline without reinventing the wrapper

What changed

  • New apps/macos/SDRMac.xcodeproj/ using Xcode 15+
    PBXFileSystemSynchronizedRootGroup — source files are
    auto-picked from the directory, no per-file UUID bookkeeping
  • SdrCoreKit stays a SwiftPM package, referenced from Xcode
    via XCLocalSwiftPackageReference
  • make mac-app / mac-app-debug now shell out to
    xcodebuild. New make mac-test runs XCTest via xcodebuild
  • Target naming split:
    • Target + Swift module: SDRMac (Swift forbids hyphens in
      identifiers; keeps import SDRMac working)
    • PRODUCT_NAME = sdr-rs — output is sdr-rs.app
    • PRODUCT_BUNDLE_IDENTIFIER = com.sdr.rs (matches Linux
      com.sdr.rs.desktop)
  • AppIcon.icns now committed under SDRMac/Resources/;
    regenerate via ./scripts/make-app-icon.sh SDRMac/Resources
    when data/com.sdr.rs.svg changes

Dropped

  • apps/macos/Package.swift — the parent SwiftPM that built
    the app
  • apps/macos/scripts/bundle-mac-app.sh — the hand-rolled
    wrapper / 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 Apple
    ID; gone — CODE_SIGN_STYLE = Automatic re-derives on open)
  • Any provisioning profile references
  • iOS-only settings (the template included iphoneos /
    iphonesimulator / xros / xrsimulator, we're macOS-only)

Also .gitignore'd xcuserdata/ at both the .xcodeproj and
inner .xcworkspace levels — 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 = macosx
  • PRODUCT_NAME = sdr-rs, PRODUCT_MODULE_NAME = SDRMac
  • PRODUCT_BUNDLE_IDENTIFIER = com.sdr.rs
  • GENERATE_INFOPLIST_FILE = NO +
    INFOPLIST_FILE = SDRMac/Resources/Info.plist
  • CODE_SIGN_ENTITLEMENTS = SDRMac/Entitlements/SDRMac.entitlements
  • ENABLE_APP_SANDBOX = NO (non-sandbox for USB per epic spec)
  • Dropped the UI test target from the template (unused)

Test plan

  • cargo fmt --all -- --check, cargo clippy --all-targets --workspace -- -D warnings, cargo test --workspace (17 test suites green)
  • make mac-app → produces apps/macos/build/sdr-rs.app
  • open apps/macos/build/sdr-rs.app — app launches,
    window renders, Source/Radio/Display panels visible,
    toolbar works
  • Clicking Play with RTL-SDR dongle plugged in — clean
    audio on 89.1 FM (release build only, per prior
    findings)
  • make mac-test — 10 tests pass via xcodebuild
  • make swift-test — SdrCoreKit's own 12 FFI
    integration tests pass
  • App icon renders correctly in Dock + Finder

Next

M4 Metal renderer (spectrum + waterfall) lands in a follow-up
PR now that the Xcode project can host .metal shaders and
tie them into the build at compile time.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Migrated macOS app build from SwiftPM to an Xcode project; build targets now produce an app bundle via Xcode.
    • Cleaned up local build and IDE artifacts to reduce repository noise.
  • Tests
    • Added a dedicated macOS test target and Make target to run tests via Xcode.
  • Documentation
    • Updated macOS development docs and workflows to reflect Xcode-based build, debugging, and testing.

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>
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27f1083f-39b9-4430-9fa3-6c7dc77fbc09

📥 Commits

Reviewing files that changed from the base of the PR and between 17071f9 and d43c1da.

📒 Files selected for processing (2)
  • Makefile
  • apps/macos/README.md

Walkthrough

Migrates macOS app from a SwiftPM-root layout to an Xcode-driven workflow: removes apps/macos/Package.swift, adds SDRMac.xcodeproj and workspace, refactors Makefile to use xcodebuild and adds mac-test, updates .gitignore, removes the SwiftPM bundling script, and adjusts docs/tests to the new workflow.

Changes

Cohort / File(s) Summary
Repository ignores
/.gitignore
Stop ignoring SwiftPM-derived artifacts under apps/macos/ (.build/, Package.resolved removed), add Xcode per-user state ignores (SDRMac.xcodeproj/xcuserdata/, project.xcworkspace/xcuserdata/) and LLVM coverage raw files (*.profraw); keep local apps/macos/build/.
Makefile and CI targets
Makefile
Replaced SwiftPM-based macOS build/test flow with Xcode-driven commands. Added mac-test phony target; refactored mac-app/mac-app-debug to call xcodebuild using SDR_MAC_PROJ and a dedicated derived-data dir, capture xcodebuild logs, and copy built .app from derived-data to apps/macos/build/.
SwiftPM manifest removal & comments
apps/macos/Package.swift, apps/macos/Packages/SdrCoreKit/Package.swift
Deleted apps/macos/Package.swift (removed SDRMac SwiftPM package, targets, and tests). Updated comment in SdrCoreKit/Package.swift clarifying unsafeFlags -L resolution behavior when consumed by Xcode.
Xcode project and workspace
apps/macos/SDRMac.xcodeproj/project.pbxproj, apps/macos/SDRMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata
Added Xcode project defining SDRMac app and SDRMacTests targets, build phases, Debug/Release settings, entitlements, bundle IDs, product references, target dependency, and a local Swift package reference; added workspace metadata file.
Docs and tests
apps/macos/README.md, apps/macos/SDRMacTests/CoreModelTests.swift
README updated to document Xcode-driven workflow, make mac-app, make mac-test, Xcode requirements and deployment-target guidance. Test file now imports SdrCoreKit.
Removed bundling script
apps/macos/scripts/bundle-mac-app.sh
Removed the SwiftPM-based helper that manually assembled and codesigned a minimal .app; bundling now performed by Xcode.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the primary change: migrating the SDRMac macOS app build system from SwiftPM to Xcode project.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/xcode-project-migration

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 53e5f6f and 17071f9.

📒 Files selected for processing (10)
  • .gitignore
  • Makefile
  • apps/macos/Package.swift
  • apps/macos/Packages/SdrCoreKit/Package.swift
  • apps/macos/README.md
  • apps/macos/SDRMac.xcodeproj/project.pbxproj
  • apps/macos/SDRMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  • apps/macos/SDRMac/Resources/AppIcon.icns
  • apps/macos/SDRMacTests/CoreModelTests.swift
  • apps/macos/scripts/bundle-mac-app.sh
💤 Files with no reviewable changes (2)
  • apps/macos/scripts/bundle-mac-app.sh
  • apps/macos/Package.swift

Comment thread apps/macos/SDRMac.xcodeproj/project.pbxproj
Comment thread Makefile
Comment thread Makefile
## 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>
@jasonherald
jasonherald merged commit 1402675 into main Apr 17, 2026
7 checks passed
@jasonherald
jasonherald deleted the feature/xcode-project-migration branch April 17, 2026 01:19
jasonherald added a commit that referenced this pull request Apr 17, 2026
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>
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