Cocoapods script phases
August 10, 2021A few years ago Cocoapods landed a long waited functionality: script phases.
Back at the time the blog did not exist so with a little delay let's have a look on how using them through two examples: SwiftLint and SwiftGen.
This is an updated version of an article I wrote some time ago in French 🇫🇷
SwiftLint
Let's start by defining a script called "🚨 SwiftLint" right into our Podfile
. We will make it execute before code compilation.
target :myApp do
pod SwiftLint
pod ...
script_phase {
:name => '🚨 SwiftLint',
:script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
:execution_position => :before_compile
}
end
Once we run pod install
Cocoapods add the script phase into our xcodeproj build phases. SwiftLint will then be executed by Xcode (or xcodebuild) when compiling.
And... that's all 🤷♂️. As we did not define any input nor output files the script phase will get executed every time we compile.
Make sure to surround environment variable with double quotes (")
SwiftGen
target :myApp do
pod SwiftGen
pod ...
script_phase {
:name => '🛠️ SwiftGen (Generate resources)',
:script => '"${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml && touch .swiftgen.yml"',
:execution_position => :before_compile,
:input_files => ['.swiftgen.yml'],
:output_files => ['Generated/Assets.generated.swift', 'Generated/Fonts.generated.swift']
}
end
Here we added two new parameters:
- input_files to define which files, when modified, trigger the script to run while building
- output_files to define files the script generate
You can also use
{input/output}_file_lists
to define input/output files
You might wonder why adding .swiftgen.yml
as input file and touching it? Script execution is based on both input and output files. If output files exist then script won't run again unless input files changed. So to make our script run every time we have to hack a little bit.
Going deeper
Up until know we defined our script phases in Podfile
. While convenient at first it can quickly increase our file length.
To mitigate this issue and make our code more readable let's move our scripts into a dedicated file: Phasesfile
(name is arbitrary).
module ScriptPhase
def self.swiftlint
{
:name => '🚨 SwiftLint',
:script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
:execution_position => :before_compile
}
end
def self.swiftgen
{
:name => '🛠️ SwiftGen (Generate resources)',
:script => Script.swiftgen,
:execution_position => :before_compile,
:input_files => ['.swiftgen.yml'],
:output_file_lists => ['.swiftgen.outputs.xcfilelist']
}
end
end
module Script
def self.swiftgen
<<~EOS
"${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml
touch .swiftgen.yml
EOS
end
end
We can now refactor our Podfile
:
load 'Phasesfile'
target :myApp do
script_phase(Phase.swiftlint)
script_phase(Phase.swiftgen)
pod SwiftLint
pod SwiftGen
pod ...
end
Not only did we improve its readability but we can now also share our Phasesfile
with other projects if we want to.
Conclusion
Cocoapods script phases are nothing but simple and handy by giving you the ability to include your custom build steps in your Podfile
.
This feature is not yet available in Swift Package Manager but should come as part of SE-303 Extensible Build Tools.
While probably providing more functionality than Cocoapods script phases counterpart it will also be harder to read.