Xcode 14 with no Bitcode, Xcode Cloud and dSYM files for Firebase Crashlytics

It’s been a while since I wrote an article here, last time it was about the new Apple Silicon, CocoaPods and iOS dev life with these two together. It was painful, and from what I can tell - for users on CocoaPods probably still is.

A lot has changed since then for our iOS team in ClassDojo:

  • we stopped using CocoaPods shortly after my blog post and moved fully to the SPM

  • Apple released new devices so we all switched to the M1 Max MacBook Pro

  • Github Actions became our one and only CI during development process

  • MacStadium released M1 MacMinis so we could speedup our CI process and run Github Actions on them

  • and finally Apple released first version of Xcode Cloud (Beta) that I moved us to for production release process

 

Last 12 months with Xcode 13 & Xcode Cloud & Fastlane

Let’s first talk about our workflow that we used to have last couple of months (until last week).

Devs work on their branches and then open up a PR to the master branch. Github Actions trigger couple of workflows:

  1. Run swiftformat on the changed files and push to the PR

  2. Run danger and check if PR has all things it should have (updated tests, hashtag in title for proper changelog parser, …)

  3. Run tests on our CI (MacMini on MacStadium)

If things look good, and you get an approval by code reviewer, you can merge your PR to the master.

Once master detects changes, CI will again run tests but it will also notify Xcode Cloud to trigger a workflow there and Archive our app for AppStore Connect internal testing. Once Xcode Cloud is done, new build for internal users is available in TestFlight.

So far so good and easy setup.

But we also use Firebase Crashlytics to monitor our crashes and app issues, and because Bitcode was enabled by default on Xcode 13, and because the archive is done via Xcode Cloud workflow, we don’t have the archive available locally / in Xcode Organizer and we don’t have valid dSYMs at this moment. So we’ve setup another Github Actions workflow to run periodically every 30 minutes and check for dSYMs on AppStore Connect, and if there are some new ones, download them and upload the to the Firebase. The Github Actions workflow looks something like this and uses Fastlane:

.github/workflows/update_dsyms.yml

name: Update dSYMs

 on:
   workflow_dispatch:
   schedule:
   # Runs at every minute 30 of every hour of every day
     - cron: '30 * * * *'

 jobs:
   update_dsyms:
     name: Update dSYM files
     runs-on: [self-hosted, macOS]
     steps:
     - name: Checkout the Git repository
       uses: actions/checkout@v2
     - name: Prepare environment
       run: bundle install
     - name: Run update dsyms command
       run: bundle exec fastlane update_dsyms

You can see it uses custom Fastlane lane that’s really simple:

desc "Downloads dSYMs for latest uploaded build and uploads them to Firebase Crashlytics"
lane :update_dsyms do |options|
  # Download dSYM files from AppStore Connect
  download_dsyms(
      version: "latest",
      wait_for_dsym_processing: true,
      wait_timeout: 900,
      api_key_path: "fastlane/fastlane_api_key.json",
  )

  # Upload dSYM files from AppStore Connect to Firebase Cryshlatics
  sh "../Scripts/upload-symbols -gsp '../Supporting Files/GoogleService-Info.plist' -p ios #"
end

It uses Fastlane action download_dsyms with extended timeout for build processing and downloads dSYMs always for the latest build.
The second part is also easy but probably more interesting - it uses upload-symbols script provided by Firebase to upload the downloaded dSYMs to the Crashlytics. Previously, you would have this script as part of your CocoaPods folder, but with SPM, it’s not longer the case. The easiest is probably to download the file from their repo and include it in some folder in your repo like we do.

Thanks to this setup, all builds archived via Xcode Cloud are properly symbolicated in Firebase Crashlytics because we always automatically download dSYMs from AppStore Connect.

Xcode 14 & Xcode Cloud, manual dSYMs

Xcode 14 released last week removes Bitcode support and no longer uses symbolication on the backend of AppStore Connect. Why and what that means is nicely explained on StackOverflow here. But it also means that after moving to Xcode 14 on Xcode Cloud, we stopped seeing dSYMs on AppStore Connect and our Github Actions workflow started failing.

Xcode 13

dSYMs available to download on AppStore Connect by Fastlane

Xcode 14

No dSYMs available to download on AppStore Connect

I was thinking maybe the dSYMs won’t be needed (yeah, I was wrong 😂) without Bitcode and Crashlytics will be okay. But once we shipped our first Xcode 14 release, we received the warning about missing dSYMs. It was the time to try to find them, upload them, and then figure this out.

I started Googling “Xcode Cloud dsyms Xcode 14” but nothing helped me really. Usually dSYMs were available on AppStore Connect or in Xcode Organizer when you archived that locally - that was not our case. So I started digging into Xcode Cloud, I knew there are these Artifacts part of every workflow run, so I went there for our specific build number, downloaded them and luckily found what I needed.

Artifacts are objects created by Xcode Cloud workflow run including the Archive if the workflow archives your project.

We could just stop here, and on every release download the artifact, locate the dSYMs inside the xcarchive, upload them and move forward. But that’s really time consuming and I love if everything is automated in our release process.

 

Xcode 14 & Xcode Cloud, automated dSYMs

So I went back to the Apple docs about Xcode Cloud to read more about the scripts.

The ci_post_xcodebuild.sh is a good candidate so first thing that came to my mind was just utilize that to find the archive and locate the dSYMs and then upload them via Fastlane. Luckily, the list of Environment variables includes also CI_ARCHIVE_PATH which gives us path to the Archive artifact with xcarchive and dSYMs folder.

First I tried to run the script with just prints - to give me an idea where we are, what’s accessible and if we have access to the project or not.

ci_post_xcodebuild.sh

#!/bin/sh
set -e
if [[ -n $CI_ARCHIVE_PATH ]];
then
    echo "Found valid archive path, trying to upload dSYMs."

    echo "Printing LS"
    ls

    echo "Printing CD"
    cd ..

    echo "Printing LS 2"
    ls

    gem install bundler
    bundle install
fi

`cd` and `ls` work correctly, installing Bundler or anything else fails due to permissions.

That’s good, we are in the ci_scripts folder inside our project, we have access to the Scripts folder that we have in the root of our project, and we have path to the archive with dSYMS. We can’t install bundler, but because we already have the upload-symbols script by Firebase included in our repo, we don’t need Fastlane or anything else. So now it’s just time to combine that all together.

ci_post_xcodebuild.sh

#!/bin/sh
set -e
if [[ -n $CI_ARCHIVE_PATH ]];
then
    echo "Found valid archive path, trying to upload dSYMs."

    echo "Start uploading dSYMs"
    ../Scripts/upload-symbols -gsp '../Supporting Files/GoogleService-Info.plist' -p ios "$CI_ARCHIVE_PATH/dSYMs"

fi

It’s super simple, and it just works.

During Xcode Cloud workflow run, after the archive step, we just look for the Archive artifact, if that exists, we utilize our existing script to upload dSYMs and as a parameter we pass the dSYMs folder from the Archive .xcarchive

 

That’s it!

And here we are at the end of this blog post. When I started digging into this, I was worried this might be way more compliated than it used to be. But because we now have access to the dSYMs right in the archive, and they won’t change on AppStore Connect via backend processing through Bitcode, the whole process has simplified a lot.

It’s basically 5 lines of code that automate our dSYMs upload to Firebase and you don’t have to worry about unsymbolicated crashes anymore.

Thanks for reading!