How to run a Build-Phase script when building a standalone Swift Package in Xcode?

Hi everyone,

I'm trying to learn SPM by making a simple standalone Swift Package with Xcode 11. So far so good :slight_smile:
I'm now trying to add a Build Phase script to execute SwiftLint when I run my Package's unit-tests locally in Xcode.

While reading the SPM documentation, i found that many of the Xcode concepts I'm familiar with can be described in the Package.swift manifest (e.g. targets, products, build-settings, build-configurations...).
I found nothing about Schemes in the documentation, but I noticed that Xcode automatically creates a hidden .swiftpm directory which can then contain my Schemes :white_check_mark:.
But I still haven't found an easy way to run a Build-Phase script, like in my other Xcode projects.

I tried creating a dummy Xcode project inside my Package's root directory, which would then reference the Package itself and add a Build-Phase script here. But with this kind of circular reference, it feels like I'm doing it wrong. Also my dummy project would get included in every App that depends on my Package.
I also thought about creating a dummy project inside another Git repository that then depends on my Package. But having to create 2 repositories for every Package also feels wrong.

Some tutorials suggest running scripts as a Git pre-commit hook, or maybe as a Scheme pre-action, but I would lose the nice integration of SwiftLint with Xcode's source code editor.

Has anyone found an elegant solution to this problem? Ideally a solution that would also work nicely if I decide to add my Package to a CI system. Am I missing something obvious?

Thank you!

3 Likes

(Disclaimer: This information relates to Xcode 11. I haven’t tried any of this in the Xcode 12 betas to know if any of it is out of date.)

The Xcode project will be completely ignored by any package clients.

SwiftPM has no equivalent of custom schemes. There is --configuration debug and --configuration release and that is all. (Well, you can add stuff to .swiftpm for some development time tweaks, but clients will not inherit it and you cannot inject build steps.)

External scripts are not supported. The feature you would need to do this “right” (known usually as “extensible build tools” been much talked about but not implemented so far.

What I do for static analysis tools providing inline warnings is to script swift package generate-xcodeproj and the insertion of a build phase. Then I quickly run that when I want to find a bunch of warnings quickly. But I don’t keep the Xcode project around all the time.

If you have CI, just add it as a separate step outside of swift test.

Since SwiftLint is itself a package, an alternative workaround would be to add it as a dependency of your test target. Then you can use #file or other shenanigans to locate the produced executable and launch it with Process inside the tests. (But to be honest I don’t think static analysis belongs inside unit tests in the first place, although that is just my opinion.)

3 Likes

Thanks for your help Jeremy.
Can you please explain how you script "the insertion of a build phase"?
I assume I will have to edit the .pbxproj file generated by swift package generate-xcodeproj.
Is there a command line tool that can help me do that, or some reference documentation for pbxproj files?

Yes.

I don’t know about either.

My own use case is simple and always the same, so it is hardcoded into a tool that also does a lot of other things. You could reverse engineer it from here. As a clumsy way of “just getting it working”, you could clone the linked package, replace these lines with your own bash script, then build and run that package with this:

swift run workspace refresh xcode •project /path/to/your/own/package

I’m pretty sure that would get you what you want. You could then cut out all the stuff you don’t care about, convert it into a standalone shell script or change it however else you want.

Ok nice I will try that.
Thanks again!

has anyone managed to figure this out?

In a regular Xcode project you can just add a build phase with:

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

and you will get syntax hilighting of lint errors.

Is there no way to do something similar to that when your Xcode project is a swift package?

No, there is still no way to add a script build phase to a package.

However, the script you posted does not require Xcode to have any knowledge of the project whatsoever. You can create an empty Xcode project and add that build phase to an empty target. Xcode will still parse the error messages and display them inline, even if no reference to the relevant file exists in Xcode’s file navigator.

Just to be clear, we also can't add the swiftlint code to the scheme, under Run > Pre-actions?

I have not tried it. A pre‐action belongs to a scheme though, not a build, so my instinct is that it would probably work. However, because it is disassociated from the build, it would be easy to circumvent by accident.

Extensible build tools would be the proper solution once the feature is released.

I tried it as both in the run pre-actions, and the build pre-actions, and the result was nothing. It might be that errors/ outputs of the swiftlint running in the scheme don't do anything (because its not integrated well with Xcode warnings/ errors)

Yeah, I did not really expect Xcode to parse the warnings. But I thought it would still halt the build if the script exited with an error code. Does that not even happen? (I mostly ask because other readers might only be trying to do things for which the output does not matter.)

Xcode just keeps on trucking even if a build script fails, or a file compile fails, or a link operations fails.

However, there are some circumstances that will cause it to abort early, normally if a can't progress any further. Not finding the build script file might be one of them.

This is a user preference in Xcode, General => Continue building after errors.

1 Like

Continue building after errors is feature for the build process, but that excludes the scripts you use in the scheme's build-phase script which I meant here: How to run a Build-Phase script when building a standalone Swift Package in Xcode? - #8 by Ben-xD

I think failures in the Xcode scheme scripts are just ignored, as @jonprescott said

Ah sorry, I misread that. It is correct that errors from these type of scripts will not stop the build.