Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Upload dSYMs to App Store Connect from Remote Mac

Short recommendation: ship dSYMs by setting uploadSymbols to true in ExportOptions.plist and letting xcodebuild -exportArchive deliver them with the .ipa; keep a verified raw .dSYM zip for third‑party crash services. Run this from an always‑on build Mac. A Figment M‑series plan starts at $95/month (exact chip, RAM and disk confirmed in the quote) and suits an iOS CI runner; the $29 virtual Intel macOS environment covers Intel‑compatible Xcode 26‑or‑earlier builds, including universal and arm64 cross‑compilation, but cannot execute ARM‑only binaries or run Xcode 27. Start a configuration request at /mac-hosting/start.

If your App Store Connect crash reports show frames like 0x000000010042c94c + 1004 with no function names, the dSYM for that build never reached Apple. Per Apple's own guidance, release builds put debug information in a companion .dSYM keyed to the binary by a build UUID, and the App Store only adds symbol names when you upload symbols with the build (Building your app to include debugging information). On a remote Mac this becomes an explicit CI step. This guide shows exactly where the dSYM lives, how to verify it against the binary, and how to ship it from a script using the supported export path.

Why Crash Reports Stay Unsymbolicated

App Store Connect symbolicates a stack by matching the crashing binary's UUID against the dSYM files you uploaded. If the UUID does not match — or no dSYM arrived — every frame stays as a hex offset. Two common causes on a remote Mac:

  1. The archive was uploaded through Organizer or Transporter with symbol upload disabled, or built with Debug Information Format set to DWARF without the companion dSYM.
  2. A wrong dSYM was shipped. Simulator and device builds of the same version have different UUIDs; a simulator dSYM does nothing for a device crash.

The fix is a short script that finds the right dSYM by build number, checks the UUIDs, and hands a verified artifact to the uploader on the same Mac that produced the archive.

Finding the dSYM for a Specific Build

Xcode archives live in ~/Library/Developer/Xcode/Archives, bucketed by date. Each .xcarchive bundle contains a dSYMs/ subfolder, and Info.plist records CFBundleVersion (build number) and CFBundleShortVersionString.

BUNDLE_VERSION=142
APP_NAME="YourApp"   # the product name, without .app

ARCHIVE=$(find ~/Library/Developer/Xcode/Archives -name "*.xcarchive" | while read -r a; do
  ver=$(/usr/libexec/PlistBuddy -c \
    "Print :ApplicationProperties:CFBundleVersion" "$a/Info.plist" 2>/dev/null || true)
  [ "$ver" = "$BUNDLE_VERSION" ] && echo "$a"
done | head -n 1)

[ -n "$ARCHIVE" ] || { echo "ERROR: no archive for CFBundleVersion $BUNDLE_VERSION" >&2; exit 1; }
DSYM_PATH="$ARCHIVE/dSYMs/${APP_NAME}.app.dSYM"
[ -d "$DSYM_PATH" ] || { echo "ERROR: dSYM missing at $DSYM_PATH" >&2; exit 1; }

Quote $DSYM_PATH throughout if your app name has spaces.

Verifying the UUID Matches the Binary

A UUID mismatch means the dSYM and the binary came from different compilations. Confirm before shipping:

APP_BINARY="$ARCHIVE/Products/Applications/${APP_NAME}.app/${APP_NAME}"
dwarfdump --uuid "$APP_BINARY"
dwarfdump --uuid "$DSYM_PATH"

Each line looks like UUID: 8A3F2C10-EF44-3B92-91EC-D30C7ACD5F82 (arm64) YourApp. A fat binary prints one UUID per slice; the sorted unique set from the binary must equal the sorted unique set from the dSYM. If they differ, find the archive that produced the shipped binary and use its dSYM.

Shipping dSYMs via the Supported Path

Apple's documented route is to include dSYMs in the archive export so xcodebuild -exportArchive delivers them alongside the .ipa. Set uploadSymbols to true in ExportOptions.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>        <string>app-store-connect</string>
  <key>teamID</key>        <string>YOUR_TEAM_ID</string>
  <key>uploadSymbols</key> <true/>
  <key>signingStyle</key>  <string>manual</string>
</dict>
</plist>

method values changed in recent Xcode majors; app-store-connect is the current App Store value in Xcode 16.4 (the deprecated alias app-store still resolves). Run xcodebuild -help on the exact Xcode installed on your build Mac and confirm the keys your version accepts before trusting any template. For third‑party crash services (Firebase Crashlytics, Sentry, Bugsnag, New Relic), keep the raw .dSYM zip and feed it to their own uploader (upload-symbols, sentry-cli upload-dif, etc.). The script in the next section produces that zip.

App Store Connect API key

Create a key in App Store Connect under Users and Access → Integrations → App Store Connect API per Apple's upload guidance (Upload builds to App Store Connect). Store the .p8 content and key ID in your CI secret manager. Materialise the file only for the upload step, with mode 600, and remove it on exit — see the CI snippet below.

Locate‑and‑Verify Script

#!/usr/bin/env bash
# package-dsym.sh — Usage: bash package-dsym.sh <CFBundleVersion> <AppName>
set -euo pipefail

BUNDLE_VERSION="${1:?Usage: $0 <CFBundleVersion> <AppName>}"
APP_NAME="${2:?}"

ARCHIVE=""
while IFS= read -r -d '' a; do
  v=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" \
        "$a/Info.plist" 2>/dev/null || true)
  if [ "$v" = "$BUNDLE_VERSION" ]; then ARCHIVE="$a"; break; fi
done < <(find ~/Library/Developer/Xcode/Archives -name "*.xcarchive" -print0)

[ -n "$ARCHIVE" ] || { echo "ERROR: archive not found for build $BUNDLE_VERSION" >&2; exit 1; }
echo "Archive: $ARCHIVE"

DSYM="$ARCHIVE/dSYMs/${APP_NAME}.app.dSYM"
BINARY="$ARCHIVE/Products/Applications/${APP_NAME}.app/${APP_NAME}"
[ -d "$DSYM" ]   || { echo "ERROR: dSYM missing: $DSYM" >&2; exit 1; }
[ -f "$BINARY" ] || { echo "ERROR: binary missing: $BINARY" >&2; exit 1; }

BIN_UUIDS=$(dwarfdump --uuid "$BINARY" | awk '{print $2}' | sort -u)
SYM_UUIDS=$(dwarfdump --uuid "$DSYM/Contents/Resources/DWARF/${APP_NAME}" | awk '{print $2}' | sort -u)
if [ "$BIN_UUIDS" != "$SYM_UUIDS" ]; then
  echo "ERROR: UUID mismatch" >&2
  printf 'Binary:\n%s\ndSYM:\n%s\n' "$BIN_UUIDS" "$SYM_UUIDS" >&2
  exit 1
fi

umask 077
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
ZIP="$WORK/${APP_NAME}-${BUNDLE_VERSION}.dSYM.zip"
( cd "$(dirname "$DSYM")" && zip -r -q "$ZIP" "$(basename "$DSYM")" )
cp "$ZIP" "./${APP_NAME}-${BUNDLE_VERSION}.dSYM.zip"
echo "Packaged: ./${APP_NAME}-${BUNDLE_VERSION}.dSYM.zip"

The script exits non‑zero on any missing file or UUID mismatch, so a failed packaging step cannot be mistaken for a successful upload. The code is an unexecuted example — run it against your own project and check the output locally before wiring it into CI.

Confirming the Upload Was Processed

After xcodebuild -exportArchive returns success, the upload enters a processing queue and typically becomes visible within a short time in App Store Connect → TestFlight → select the build → Build Metadata / Symbols. Treat processing times as variable, not guaranteed. For crashes from builds whose symbols reached Apple, the Crashes organiser in Xcode symbolicates them automatically; for anything else, symbolicate locally with xcrun symbolicatecrash against the same archive.

Wiring the Steps on a Remote Mac

set -euo pipefail

# 1. Archive
xcodebuild archive \
  -project YourApp.xcodeproj \
  -scheme "$XCODE_SCHEME" \
  -destination 'generic/platform=iOS' \
  -archivePath "$PWD/build/YourApp.xcarchive"

# 2. Materialise the .p8 for the upload step only
umask 077
mkdir -p ~/.appstoreconnect/private_keys
P8_PATH=~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8
printf '%s' "$ASC_P8_CONTENT" > "$P8_PATH"
trap 'rm -f "$P8_PATH"' EXIT

# 3. Export + upload (uploadSymbols=true delivers dSYMs to App Store Connect)
xcodebuild -exportArchive \
  -archivePath "$PWD/build/YourApp.xcarchive" \
  -exportOptionsPlist ExportOptions.plist \
  -exportPath "$PWD/build/export" \
  -authenticationKeyPath "$P8_PATH" \
  -authenticationKeyID "$ASC_KEY_ID" \
  -authenticationKeyIssuerID "$ASC_ISSUER_ID"

# 4. Also package the raw dSYM zip for third-party crash services
bash package-dsym.sh "$BUILD_NUMBER" "$APP_NAME"

Pass XCODE_SCHEME, ASC_KEY_ID, ASC_ISSUER_ID and ASC_P8_CONTENT from your CI secret store. Never commit the .p8, never echo it, never put secret literals into shell history. The trap … EXIT cleans up on normal exit or failure; it cannot run after a force‑killed runner, so keep the key path under a user‑private directory and rotate keys if a host is ever compromised.

For an always‑on build Mac behind this pipeline, see Figment Mac Hosting or request a quote at /mac-hosting/start. The $29 virtual Intel macOS plan handles the compile, archive and dSYM packaging for Intel‑compatible Xcode toolchains; pick an M‑series plan from $95/month when you need ARM execution, Xcode 27, or more RAM and concurrency than a 2 vCPU / 8 GiB VM provides.


Related: SwiftData Migration on a Remote Mac · XCTest Performance Testing on a Remote Mac · GitHub Actions Self-Hosted Runner on Mac


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms