Swift/UnwrapDiscover about Swift, iOS and architecture patterns

Running a package from Xcode Build phase

June 25, 2024

I’ve been recently struggling with Xcode while I was facing (twice) the need of running a SPM package from it. First with Apollo after migration to 1.x and which does no more support generating models from Build phase. Then with https://github.com/pjechris/AnnotationInject (my homemade dependency injection library) where someone also asked how to integrate it with Xcode.

After many searches and failures here is the final solution I came up with!

The problem

But first let me explain you why I needed to do this. Thos libraries don’t have SPM Build plugin support (which is not a great tool, sorry!) and so I needed to do swift package run in order to use them.

But anyone who ever tried to run swift package run <my_package> from Build Phase knows that… it just plainly fails 😬

Why is it so? I suspect Apple felt it would be fun for developers to have different behaviours when including packages either using Package.swift or Xcode 🤭: indeed using Package.swift you get your dependencies downloaded into the root tree while with Xcode… it’s just somewhere. And finding where this somewhere is is most of the job!

Où k’il est ?

Where is Xcode hiding storing those packages? It wasn’t easy to figure out because it’s not documented and no environment variable ever makes reference to it 😤.

After searching for a while it actually seems those are stored in <derived_data>/<project>/SourcePackages.

Luckily for us… no variable ever refer to derived_data ever 😄. I’m starting to think Apple loves developers 🥹❤️.

We’ll have to rely on some other environment variable, BUILD_DIR, and tweak it a little bit. I ended up with this script (for AnnotationInject), hoping it would work 🤞:

SPM_CHECKOUT_DIR=${BUILD_DIR}/../../../Build/SourcePackages/checkouts/AnnotationInject
cd $SPM_CHECKOUT_DIR
/usr/bin/xcrun swift run annotationinject-cli ...

But… it didn’t 😅. Xcode just throws an error: « using sysroot for 'iPhoneSimulator' but targeting 'MacOSX' ». Some google search later, setting the SDK to macOS fixes the issue:

SPM_CHECKOUT_DIR=${BUILD_DIR}/../../../Build/SourcePackages/checkouts/AnnotationInject
cd $SPM_CHECKOUT_DIR
/usr/bin/xcrun --sdk macosx swift run annotationinject-cli ...

And it’s working… for now! You thought it was ever? It’s not 😄.

What about production?

So everything is working… until you decide to release your app. And then something terrible happens: the script doesn’t work 😱.

The reason is pretty « simple » : BUILD_DIR doesn’t match the same patch when compiling in RELEASE mode so our little trick don’t work anymore 😈.

Thankfully the solution is, again, pretty simple:

SPM_CHECKOUT_DIR=${BUILD_DIR%Build/*}SourcePackages/checkouts/AnnotationInject
cd $SPM_CHECKOUT_DIR
/usr/bin/xcrun --sdk macosx swift run annotationinject-cli ...

Using this syntax trims everything after Build inside the variable, which allows to adapt seamlessly for both DEBUG and RELEASE 🎉.

Bonus : Apollo

If you ever need, here is the script I wrote for downloading apollo cli and generating files:

SPM_CHECKOUT_DIR=${BUILD_DIR%Build/*}SourcePackages/checkouts/apollo-ios

if [ ! -f ${SRCROOT}/apollo-ios-cli ]; then
    echo "Downloading missing apollo cli"
    sh ${SPM_CHECKOUT_DIR}/scripts/download-cli.sh ${SRCROOT}
fi

cd ${SRCROOT}

./apollo-ios-cli generate

find Generated/Apollo -type f > .apollo_generated.xcfilelist

Note the last line. By generating a xcfilelist containing all generated filenames we can add them to Xcode with no versioning while also supporting fresh builds (when files were not generated yet).

Conclusion

Now you know it’s possible to use swift run from Build Phase and how to do so. It can be pretty useful when having to run a package not supporting SPM Command plugin.

It’s just a shame we have nothing more «straightforward». Hopefully Xcode integration will get better one day like… allowing to use our own Package.swift? 🤷‍♂️