Support Swift macros with CocoaPods
Guide to distribute your macros using 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 Package.swift
manifest:
- 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 inprovidingMacros
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:
-load-plugin-executable <path>#<module-names>
Path to an executable compiler plugins and providing module names such as macros
-load-plugin-library <path>
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 podspec
:
- Macro definition files are added to
source_files
. - The path to macro executable is added to
preserve_paths
so that CocoaPods doesn’t delete it afterpod install
. - The macro executable path and module name argument are provided to
user_target_xcconfig
asOTHER_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 podspec
:
Package.swift
manifest and macro source files are added topreserve_paths
instead to make sure these files don’t get deleted by CocoaPods afterpod install
.- 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 productMyMacroMacros
is built withrelease
configuration.
- Path to host machine SDK is provided withxcrun — show-sdk-path
, as macro will be built to run on host machine.
- The path toPackage.swift
manifest directory is provided with— package-path
option and the location of macro target build files provided with— scratch-path option
. - In the
script_phase
,Package.swift
manifest and macro source files are provided asinput_files
and built macro executable is provided asoutput_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. - The
script_phase
runs 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
user_target_xcconfig
asOTHER_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.
Conclusion
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 MetaCodable
‘s Package.swift
manifest and podspec
s.