By the release of Swift 5.9, it provides the feature Swift Macro
, which is really useful for the developer to reduce boilerplate code and helping to improve the readability of the code.
However, as we know, currently most long running projects are using CocoaPods as their dependency manager, while the Swift Macro support officially relies on SwiftPM. This prevents macros from being directly used in the project code and development pods.
So this article is about to introduce how to make Swift Macro available using CocoaPods, for host project and other pods.
Key Point - using an executable macro plugin
Inspired by the information in this discussion and this post, it shows that we can provide an executable binary to the Swift Compiler in Xcode settings: add -load-plugin-executable <path-to-plugin-executable>#<executable-module-name>
to OTHER_SWIFT_FLAGS
.
For example:
1
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable Resources/Macros/MyMacroPlugin#MyMacroPlugin',
That means we can build a plugin executable and provide it through CocoaPods, update the settings in Pods project and host project before or after the pod install
command.
Okay, let’s do it.
(All the example code can be found in this repo)
Create a macro plugin executable
We can easily create a demo macro project using Xcode 15 or command line swift package init --type macro
. And then update the Package.swift
like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
import CompilerPluginSupport
let package = Package(
name: "SwiftyArchitectureMacros",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
// Product which is a executable plugin for other project's compiler to integrate.
.executable(
name: "SwiftyArchitectureMacros",
targets: ["SwiftyArchitectureMacros"]),
],
dependencies: [
// Depend on the Swift 5.9 release of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
.executableTarget(
name: "SwiftyArchitectureMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
// A test target used to develop the macro implementation.
.testTarget(
name: "MacrosTests",
dependencies: [
"SwiftyArchitectureMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
There are some important informations:
The target
SwiftyArchitectureMacros
must be a executable target, normally it’s a.macro
target. And the product of the executable should target on the executableSwiftyArchitectureMacros
.Second, by several tests, the executable target should contain original macro files, not macro definition files. So it means that the definition of the macro should be contained in the pod we build.
By using this swift build -c release
command, we can get a SwiftyArchitectureMacros
executable file in .build/release/
.
Create a pod to host the macro plugin executable
Using a prepared macro executable
Next, we will create a podspec file to host the executable file and add some configurations.
We can create a .podspec
now, and in my case it is SwiftyArchitectureMacrosPackage.podspec
. The key content is below.
1
2
3
4
5
6
7
8
9
10
11
12
s.source_files = 'Sources/MacrosDefine/*'
s.preserve_paths = 'Products/**/*'
xcode_config = {
'OTHER_SWIFT_FLAGS' => <<-FLAGS.squish
-Xfrontend -load-plugin-executable
-Xfrontend $(PODS_ROOT)/SwiftyArchitectureMacrosPackage/Products/SwiftyArchitectureMacros#SwiftyArchitectureMacros
FLAGS
}
s.user_target_xcconfig = xcode_config # <-- add to the `Host project`.
s.pod_target_xcconfig = xcode_config
Key points:
The
source_files
should contain the macro definition files, which are the files that contains the macro definitions like:1 2
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "SwiftyArchitectureMacros", type: "StringifyMacro")
- The
preserve_paths
should contain the executable file, which we build before and move it to a folder, likeProducts/
. - The
user_target_xcconfig
andpod_target_xcconfig
should contain the same configurations, which integrate the executable to compiler plugin. - The
s.user_target_xcconfig
is used to modify settings of the host project, while thes.pod_target_xcconfig
is used to modify settings of the current pod target.
Using a script to build a executable
Inspired by this post, we can also use a script to build the executable.
We can create another .podspec
file, which is SwiftyArchitectureMacros.podspec
in my case, and the key content is below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
s.source_files = 'Sources/MacrosDefine/*'
s.preserve_paths = 'Package.swift', 'Sources/**/*', 'Tests/**/*'
product_folder = "${PODS_BUILD_DIR}/Products/SwiftyArchitectureMacros"
script = <<-SCRIPT.squish
env -i PATH="$PATH" "$SHELL" -l -c
"swift build -c release --product SwiftyArchitectureMacros
--package-path \\"$PODS_TARGET_SRCROOT\\"
--scratch-path \\"#{product_folder}\\""
SCRIPT
s.script_phase = {
:name => 'Build SwiftyArchitectureMacros macro plugin',
:script => script,
:input_files => Dir.glob("{Package.swift, Sources/**/*}").map {
|path| "$(PODS_TARGET_SRCROOT)/#{path}"
},
:output_files => ["#{product_folder}/release/SwiftyArchitectureMacros"],
:execution_position => :before_compile
}
Besides the key points introduced above, this section also needs attention to several other key points:
- The
preserve_paths
should contain thePackage.swift
and files that are used to build the executable. - And don’t forget to update the build config path to
#{product_folder}/release/SwiftyArchitectureMacros#SwiftyArchitectureMacros
.
The benefit is that we don’t need to prepare the executable file, it will be built by the script when the main project starts building, and it won’t have any compatible issue. However you should know that the script will be executed every time when the project builds, and it may takes a long time when there’s no build cache. So I suggest as a SDK provider, you should provide both options.
Integrate to other targets
Host project
If your main codes are in the host project, you can integrate the macro plugin to the host project by adding the following code to the Podfile
.
1
2
3
pod 'SwiftyArchitectureMacrosPackage'
#or
pod 'SwiftyArchitectureMacros'
Because of the OTHER_SWIFT_FLAG
setting are already inserted by the podspec
file into the host project settings, you don’t need to do anything else.
1
2
3
4
5
6
7
8
import SwiftyArchitectureMacrosPackage
func test() {
let a = 1
let b = 2
let desc = #stringify(a + b)
print(desc)
}
It works fine~
Used by other pods or development pods
First, add the dependency in the other’s podspec:
1
s.dependency 'SwiftyArchitectureMacrosPackage'
And then it will be a little tricky, because we can’t directly insert the OTHER_SWIFT_FLAG
into other pod target settings because:
- In another’s podspec, hard code the executable path is not a good idea, because the path may changes when you switching the macro pod between local and remote.
- If a lot of pods are depending on the macro pod, when macro pod setting changes you must update all the pods’ podspec file which is a big trouble.
So we need to do some tricks. We can use pod install
’s post_install
hook to do this. Add these code to your Podfile
, it aims to add the OTHER_SWIFT_FLAG
setting to the Pods.xcodeproj
, and all the pod targets will inherit from it.
1
2
3
4
5
6
7
8
9
post_install do |installer_representation|
macro_product_folder = "${PODS_BUILD_DIR}/Products/SwiftyArchitectureMacros"
installer_representation.pods_project.build_configurations.each do |config|
config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -load-plugin-executable #{macro_product_folder}/release/SwiftyArchitectureMacros#SwiftyArchitectureMacros"
end
end
Now try pod install
and see if all the pod targets are inheriting correctly from the Pods.xcodeproj
. If so, you can use the macros in your codes now.