Swift Package Plugins: Part 2 — Build Tool Plugins
Before we get started, if you’re new to Swift package plugins, I highly recommend checking out my article: Swift Package Plugins: Part 1 —…
Swift Package Plugins: Part 2 — Build Tool Plugins

Before we get started, if you’re new to Swift package plugins, I highly recommend checking out my article: Swift Package Plugins: Part 1 — Introduction. In that article, I introduce Swift package plugins, explain the different types and their capabilities, and explore the use cases for each. In this tutorial, we’ll dive deeper into Build Tool Plugins, with a specific focus on In-Build Plugins.
Build Tool Plugins
Build tool plugins are Swift package plugins that integrate directly into the build process of your project, running as part of every build. They allow you to customise and automate tasks during the build lifecycle. These plugins are categorised into two types:
- Pre-Build Plugins
- Pre-build plugins are executed before the build process begins.
- They are ideal when the name of the output files is not known beforehand or in other words cannot be determined before the command is run — this is the case if the contents of the input files (as opposed to just their names) determine the number and names of the output files.
- However, because pre-build commands run before each build, the build system does not handle caching for them. Therefore, you must implement your own caching mechanism to minimise work and prevent slowing down incremental builds.
- The plugin will return a
prebuildCommand
return [.prebuildCommand(
displayName: "Running SomeTool",
executable: try context.tool(named: "SomeTool").path,
arguments: [ "--verbose", "--outdir", outputDir ],
outputFilesDirectory: outputDir)
]
2. In-Build Plugins
- In-build plugins run during the build process and are suitable when the paths to both input files and the expected output files are known ahead of time.
- These plugins benefit from the build system’s built-in dependency tracking and caching. The build system automatically determines whether the command needs to be re-run by checking: — Whether the outputs are missing — When the inputs have changed since the last time the command ran
- Since the build system handles caching, you don’t need to implement a custom cache mechanism for these plugins.
- The plugin returns a
buildCommand
return [.buildCommand(
displayName: "Generating \(outputName) from \(inputPath.lastComponent)",
executable: try context.tool(named: "SomeTool").path,
arguments: [ "--verbose", "\(inputPath)", "\(outputPath)" ],
inputFiles: [ inputPath, ],
outputFiles: [ outputPath ]
)]
In either case, it’s important to understand that the plugin itself doesn’t perform the work of the build command. Instead, the plugin’s role is to construct the commands that will be executed later. These commands are responsible for carrying out the actual tasks. The plugin is typically lightweight and mainly focuses on assembling the command-line arguments for the build command.
Lets Get Started
In this tutorial, we will focus on creating an In-Build Plugin to automatically generate a Swift enum for fonts added to your project. This plugin will streamline font management by providing a strongly-typed, auto-generated enum that allows you to easily reference fonts in your code.
Step 1: Create a Swift Package
As mentioned in the earlier article, plugins are distributed as part of a Swift package. The first step is to create a Swift package. You can name it anything you like; in this example, I’ll call it FontEnum.

Package.swift file of Newly created swift package
Step 2: Define the Plugin in Package.swift
- Open the Package.swift file.
- Add a new target for the plugin:
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "FontEnum"),
.plugin(name: "FontEnumGenerator", capability: .buildTool())
]
- Update the products section if you want to make your plugin available to external projects or other Swift packages. If you only intend to use the plugin within the package it’s defined in, you can skip this step.
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.plugin(name: "FontEnumGenerator", targets: ["FontEnumGenerator"])
]
- This step is optional, but in this example, I will remove the FontEnum target from the products section since we won’t be importing anything from it .

Package.swift file after following steps 1 to 4
Step 3: Implement the Plugin
- Create a new folder named Plugins in your package directory. If you prefer to use a different name for the folder, you must specify the custom path for the plugin target in the targets section of your Package.swift file.
- Within the Plugins folder, create a subfolder named FontEnumGenerator, and inside it, add a new Swift file called FontEnumGenerator.swift.
FontEnum/
├── Package.swift
├── Sources/
│ └── FontEnum
└── Plugins/
└── FontEnumGenerator/
└── FontEnumGenerator.swift
- Implement Plugin:
import PackagePlugin
import Foundation
@main
struct FontEnumGenerator: BuildToolPlugin {
// This method creates the build commands which will be run by the build system when necessary
func createBuildCommands(context: PackagePlugin.PluginContext, target: any PackagePlugin.Target) async throws -> [PackagePlugin.Command] {
// The executable responsible for generating the Swift enum file from font files
let fontGenerator = try context.tool(named: "FontEnumGeneratorExc")
let targetName = target.name
// Ensure the target has a source module (to retrieve input files)
guard let sourceModule = target.sourceModule else {
throw PluginError.missingSourceModule
}
// Filter the source files in the target to include only font files with valid extensions
let inputFiles = sourceModule.sourceFiles.filter { hasValidFontExtension($0.url) }.map{ $0.url }
let inputFilesArguments = inputFiles.map{ $0.path() }
// If no valid font files are found, throw an error and display a diagnostic message
if inputFiles.isEmpty {
Diagnostics.error("The target \(targetName) does not contain any custom fonts in a supported format. Supported formats are: .ttf, .otf.")
throw PluginError.missingInputFiles
}
// Define the output directory where the generated enum file will be stored
let outputDirectory = context.pluginWorkDirectoryURL
.appending(path: target.name)
.appending(path: "Generated")
// Create the output directory if it doesn't already exist
try FileManager.default.createDirectory(
at: outputDirectory,
withIntermediateDirectories: true
)
// Define the path for the generated Swift file
let outputFile = outputDirectory.appending(path: "\(targetName)GeneratedFonts.swift")
// Return the build command to execute the font generator tool
return [
.buildCommand(
displayName: "Generating Font Definitions For \(targetName)", // Display name for the build process
executable: fontGenerator.url, // Path to the executable tool
arguments: [
outputFile.path(), // Output file path as the first argument
] + inputFilesArguments , // Append input font file paths as arguments
environment: [:], // Specify any custom environment variables if needed
inputFiles: inputFiles, // Declare input files for tracking changes
outputFiles: [outputFile] // Declare the output file for tracking changes. Files which are declared here will be included in the bundle when the app is archived
)
]
}
}
private extension FontEnumGenerator {
// Helper method to check if a file has a valid font extension (.ttf or .otf)
func hasValidFontExtension(_ fileUrl: URL) -> Bool {
let fileName = fileUrl.lastPathComponent
return fileName.hasSuffix(".ttf") || fileName.hasSuffix(".otf")
}
}
// Enum to define possible plugin errors
enum PluginError: Error {
case missingInputFiles // Error for missing font files
case missingSourceModule // Error for missing source module
}
- This step is optional, but if you want your plugins to be available for Xcode projects — or in other words, to work within Xcode projects — add the following code:
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
// Extend FontEnumGenerator to conform to XcodeBuildToolPlugin
// This enables the plugin to work with Xcode projects
extension FontEnumGenerator: XcodeBuildToolPlugin {
func createBuildCommands(context: XcodeProjectPlugin.XcodePluginContext, target: XcodeProjectPlugin.XcodeTarget) throws -> [PackagePlugin.Command] {
// Retrieve the tool (executable) defined in the package
let fontGenerator = try context.tool(named: "FontEnumGeneratorExc")
let targetName = target.displayName // Get the human-readable name of the target
// Filter input files to include only supported font files
let inputFiles = target.inputFiles.filter { hasValidFontExtension($0.url) }.map{ $0.url }
let inputFilesArguments = inputFiles.map{ $0.path() }
// If no valid font files are found, throw an error and show a diagnostic message
if inputFiles.isEmpty {
Diagnostics.error("The target \(targetName) does not contain any custom fonts in a supported format. Supported formats are: .ttf, .otf.")
throw PluginError.missingInputFiles
}
// Define the output directory where the generated file will be stored
let outputDirectory = context.pluginWorkDirectoryURL
.appending(path: target.displayName)
.appending(path: "Generated")
// Create the output directory if it doesn't already exist
try FileManager.default.createDirectory(
at: outputDirectory,
withIntermediateDirectories: true
)
// Define the output file path for the generated Swift file
let outputFile = outputDirectory.appending(path: "\(targetName)GeneratedFonts.swift")
// Construct the build command to execute the tool
return [
.buildCommand(
displayName: "Generating Font Definitions For \(targetName)", // Display message during the build process
executable: fontGenerator.url, // Path to the executable tool
arguments: [
outputFile.path(), // Specify the output file path as the first argument
] + inputFilesArguments , // Append the font file paths as additional arguments
environment: [:], // Specify any environment variables if required
inputFiles: inputFiles, // Declare input files for dependency tracking
outputFiles: [outputFile] // Declare the generated file as an output in order to be included when app is archived
)
]
}
}
#endif
Plugins do not have permission to create the output directory themselves, so you must ensure the output directory exists beforehand, typically within the plugin’s implementation. When constructing the build command, you need to specify the path to the executable (a command-line tool defined as part of the package or elsewhere), which performs the actual operations such as file generation or transformation. The plugin’s primary responsibility is to construct the command that invokes this executable, providing any necessary arguments, environment variables, or file paths, while the executable handles the core processing logic.
Step 4: Create the Executable Tool
- Add a new target for the executable and specify it as a dependency for the plugin in Package.swift:
.executableTarget(name: "FontEnumGeneratorExc"),
.plugin(name: "FontEnumGenerator", capability: .buildTool(), dependencies: ["FontEnumGeneratorExc"])

Package.swift file after adding the executable target
- Implement the executable in Sources/FontEnumGeneratorExc/FontEnumGeneratorExc.swift:
FontEnum/
├── Package.swift
├── Sources/
│ ├── FontEnum/
│ │
│ └── FontEnumGeneratorExc/
│ └── FontEnumGeneratorExc.swift
└── Plugins/
└── FontEnumGenerator/
└── FontEnumGenerator.swift
import Foundation
// Main structure representing the executable responsible for generating the font enum
@main
struct FontEnumGeneratorExc {
// Entry point of the executable
static func main() throws {
// Ensure the command line arguments are valid
// The minimum required arguments: executable name, output path, and at least one input file
guard CommandLine.arguments.count > 3 else {
throw Error.invalidArguments
}
let arguments = CommandLine.arguments
let outputPath = arguments[1] // Second argument specifies the output file path
let inputFiles = Array(arguments[2...]) // Remaining arguments are the input font files
let enumName = "AppFont" // Name of the generated Swift enum
// Generate enum cases for each input file
let cases = try inputFiles.compactMap { path -> String? in
let url: URL?
// Use modern file path API if available, fallback to older API for compatibility
if #available(iOS 16.0, macOS 13.0, *) {
url = URL(filePath: path)
} else {
url = URL(fileURLWithPath: path)
}
// Extract the file name (without extension) from the URL
guard let fileName = url?.deletingPathExtension().lastPathComponent else {
throw Error.invalidArguments
}
// Generate a valid case name by removing invalid characters and formatting
let caseName = fileName
.replacingOccurrences(of: "-", with: "")
.replacingOccurrences(of: " ", with: "")
.lowercasingFirstLetter()
// Create the enum case declaration
return " case \(caseName) = \"\(fileName)\""
}.joined(separator: "\n")
// Define the Swift content for the enum
let swiftContent = """
enum \(enumName): String {
\(cases)
}
"""
// Write the generated enum to the specified output file
try swiftContent.write(
to: URL(fileURLWithPath: outputPath), // Output path for the file
atomically: true, // Write atomically to avoid partial writes
encoding: .utf8 // Use UTF-8 encoding for the Swift file
)
}
}
// Enum to define errors for invalid command line arguments
enum Error: Swift.Error {
case invalidArguments // Raised when the arguments are insufficient or invalid
}
// String extension to lowercase the first letter of a string
extension String {
func lowercasingFirstLetter() -> String {
return prefix(1).lowercased() + dropFirst() // Lowercase the first character
}
}
Step 5: Add Fonts to Your Project
- Create a Resources/Fonts directory in your project.
- Add your .ttf or .otf font files to this directory.
Step 6: Integrate the Plugin
In Your Xcode Project
- Add the Swift package as a dependency to your Xcode project.
- Link the plugin to your target in Xcode: — Select your target. — Go to Build Phases > Run Build Tool Plug-Ins. — Add the FontEnumGenerator plugin.

Adding plugin to Xcode project
Within the Same Package
If you are using the plugin within the same Swift package where it was defined, update the targets section of your Package.swift file to link the plugin to the target that requires it. For example:
.target(
name: "FontEnum",
plugins: [.plugin(name: "FontEnumGenerator")]
),
In Another Swift Package
- Add the FontEnum package as a dependency in the dependencies section of the Package.swift file for the other package.
- Specify the plugin in the targets section of the Package.swift file for the target where you want to use it.
// Step 1
dependencies: [
.package(url: "https://github.com/your-repo/FontEnum", from: "1.0.0")
]
// Step 2
.target(
name: "YourTarget",
dependencies: [],
plugins: [
.plugin(name: "FontEnumGenerator", package: "FontEnum")
]
)
Step 7: Build and Verify
- Build your project.
- Use the generated AppFonts enum in your code:
let customFont = UIFont(name: AppFonts.OpenSans, size: 16)
The complete FontEnum example project is available on GitHub. Feel free to explore it for a detailed understanding and hands-on experience with the implementation.
Conclusion
With this build tool plugin, specifically the in-build plugin, you’ve automated the process of managing font names in your project. By leveraging the capabilities of a build tool plugin, it integrates seamlessly into the build process, reducing manual effort, minimising errors, and ensuring your code stays in sync with the fonts added to your project. This approach not only improves efficiency but also highlights the power and flexibility of Swift package plugins in simplifying development workflows.🚀
Reference Articles
메타데이터
- post_id
- 2228e2ff4ae5
- slug
- swift-package-plugins-part-2-build-tool-plugins-2228e2ff4ae5
- url
- https://medium.com/@rohanbimalraj/swift-package-plugins-part-2-build-tool-plugins-2228e2ff4ae5
- canonical_url
- https://medium.com/@rohanbimalraj/swift-package-plugins-part-2-build-tool-plugins-2228e2ff4ae5
- author_url
- https://medium.com/@rohanbimalraj
- status
- ok
- fetched_at
- 2026-09-02 06:28:33