Support Swift macros with CocoaPods
With the introduction of macros in Swift 5.9, removing common boilerplate from code has never been easier in Swift. However, the development of macros is more closely tied to Swift Package Manager, and depending on your use cases this might be a limitation to you, whether you are a macro library author losing out on CocoaPods users, or you are planning to introduce macro to an existing code base that hasn’t adopted SwiftPM.
In this article, we will discover how to create CocoaPods macro library from a Swift macro package and distribute it similar to any other CocoaPods library.
Basics of SwiftPM Macro library
Xcode 15 supports creating macro library with SwiftPM right out of the box with custom template Swift Package template:
When you create a a macro package, you will notice following things in your
- A macro target containing macro implementation which uses swift-syntax as dependency.
- A library target containing macro definition and uses macro target as dependency.
When going into macro target you will find a type that conforms to
CompilerPlugin with all the macros provided in
providingMacros property, and has the
@main attribute attached:
You might have noticed this
@main attribute before to declare application execution entry point. In fact, a macro is also an application/program, that generates some code output based on input. The only difference is that macros run on the host platform (i.e. macOS, the platform that builds application) not on the target platform (i.e. iOS, tvOS etc. the platform application will run on). This allows macros to not introduce additional overhead when user is running the application.
In the next section, we will see how Swift allows macro executables to be passed directly without SwiftPM usage.
Macros outside SwiftPM
Swift exposes two input options for macros to be injected directly from command line:
Path to an executable compiler plugins and providing module names such as macros
Path to a dynamic library containing compiler plugins such as macros
From the previous section we know that macro is a simple program and now we can build it as an executable and provide executable path and macro module name to Swift in
-load-plugin-executable to use the macro executable:
Now that we have the macro executable ready, we can move to creating the
podspec for our CocoaPods macro library.
Create CocoaPods macro library
Now we can create macro library
podspec, that will provide the macro executable to Swift:
Few things to note here in the
- Macro definition files are added to
- The path to macro executable is added to
preserve_pathsso that CocoaPods doesn’t delete it after
- The macro executable path and module name argument are provided to
OTHER_SWIFT_FLAGS, the executable must be provided to user/app target not to our library target.
While this will work just like other CocoaPods library, the major limitation of this approach is you need the macro executable beforehand. As a library author, this introduces additional challenge of building executable as part of Continuous Deployment workflow, choosing additional host for distributing with CocoaPods or to add the executable among source files itself (making the package cloning for SwiftPM slower). In the next section, we will see how we don’t need to have pre-built executable and distribute macro implementation source files just like we do in SwiftPM.
Bringing CocoaPods macro library to SwiftPM level
The only approach to use macro outside SwiftPM is to provide the macro product from command line, but we don’t have to build the macro product before hand, we can add the macro product building task as
script_phase and provide the built executable to Swift.
While you can use other tools like
cmake, here we will use SwiftPM to build our macro executable using the same
Package.swift manifest, and update the
podspec with new details:
Additional things to note in our new
Package.swiftmanifest and macro source files are added to
preserve_pathsinstead to make sure these files don’t get deleted by CocoaPods after
- Xcode build environment variables are not passed to Swift build task using command
env -i PATH=”$PATH” “$SHELL” -l -c, to allow Swift building for our host platform not target platform.
- In the Swift build task:
- Macro executable product
MyMacroMacrosis built with
- Path to host machine SDK is provided with
xcrun — show-sdk-path, as macro will be built to run on host machine.
- The path to
Package.swiftmanifest directory is provided with
— package-pathoption and the location of macro target build files provided with
— scratch-path option.
- In the
Package.swiftmanifest and macro source files are provided as
input_filesand built macro executable is provided as
output_files. This allows the build task to only run if there is change in source files or the executable product hasn’t been built yet, optimizing the build process.
script_phaseruns before compiling allowing the macro executable to be provided to Swift build process.
- The new macro executable path and module name argument are provided to
OTHER_SWIFT_FLAGS, the executable must be provided to user target/app not to our library target.
Advantages over SwiftPM
The subtle advantages of distributing macros with CocoaPods in this approach over SwiftPM are :
- The deployment target needs to be set higher to allow swift-syntax usage, in case of SwiftPM. By separating macro definitions from the implementation, we can set lower deployment target for our macro library.
- In case of SwiftPM, swift-syntax version will be resolved to a common version, thereby introducing chances of version conflicts. While here, since macro implementation is separate, each CocoaPods macro library can use separate swift-syntax version independently.
By following these steps, now macro libraries can also be distributed as CocoaPods library, same as we can already distribute as Swift package. The library can be included by Xcode targets in the same way as any other CocoaPods library. For more concrete example, you can have a look at my
Codable macro library
Package.swift manifest and