Support Swift macros with CocoaPods

Guide to distribute your macros using CocoaPods

Soumya Mahunt
5 min readJan 29, 2024

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 after pod install.
  • The macro executable path and module name argument are provided to user_target_xcconfig as 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 podspec:

  • Package.swift manifest and macro source files are added to preserve_paths instead to make sure these files don’t get deleted by CocoaPods after pod 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 product MyMacroMacros is built with release configuration.
    - 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.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 as input_files and 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.
  • 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 as 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.

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 podspecs.

--

--

Soumya Mahunt

Senior Software Engineer at MoEngage | System architecture enthusiast | Ex Tataneu