Swift Package Manager is no longer just a dependency management tool — it's a full-fledged build automation platform. Package Plugins, introduced with Swift 5.6, let you add custom steps to your build process, generate code, and automate repetitive tasks in your project. If you're still running manual linting, code generation, or documentation scripts, this guide will change your life.
Note: All examples in this guide have been tested with Swift 5.9+ and Xcode 15+.
Table of Contents
- 1. What Are Package Plugins?
- Why Should You Use Plugins?
- 2. Detailed Comparison of Plugin Types
- 3. Creating a Build Tool Plugin
- Basic Structure
- Pre-Build vs Build Command
- 4. Creating a Command Plugin
- 5. Real-World Examples
- Example 1: Mock Generator Plugin
- Example 2: Asset Catalog Validator
- 6. Plugin Security Model
- Sandbox Rules
- 7. CI/CD Integration
- Conclusion and Recommendations
1. What Are Package Plugins?
Swift Package Plugins are tools written in Swift that let you hook into the SPM build process. There are two main types:
- Build Tool Plugins: Run automatically during compilation (code generation, asset processing)
- Command Plugins: Triggered manually (linting, formatting, documentation)
Both types run inside a sandbox and have limited access to your system.
Why Should You Use Plugins?
- Eliminates repetition: The scripts you used to run before every build are now automatic
- Platform independent: macOS, Linux — works the same way everywhere
- Type-safe: Plugins are written in Swift, giving you compile-time safety
- Shareable: You can distribute plugins as a package
1// Package.swift - Plugin definition2// swift-tools-version: 5.93import PackageDescription4 5let package = Package(6 name: "MyApp",7 products: [8 .library(name: "MyApp", targets: ["MyApp"])9 ],10 targets: [11 // Main target12 .target(13 name: "MyApp",14 plugins: [15 .plugin(name: "CodeGenPlugin")16 ]17 ),18 // Plugin target19 .plugin(20 name: "CodeGenPlugin",21 capability: .buildTool()22 )23 ]24)2. Detailed Comparison of Plugin Types
Feature | Build Tool Plugin | Command Plugin |
|---|---|---|
Runtime | Automatic on every build | Manual trigger |
Sandbox | Full sandbox | Restricted (expands with permission) |
File Writing | Build directory only | Project directory (with permission) |
Use Case | Code generation, asset processing | Linting, formatting, deploy |
Performance Impact | Can extend build time | Doesn't affect build time |
Xcode Support | Full integration | From the right-click menu |
CI/CD | Runs automatically | Via the swift package command |
3. Creating a Build Tool Plugin
Build Tool Plugins run before every compilation and are typically used to generate source code.
Basic Structure
1// Plugins/CodeGenPlugin/CodeGenPlugin.swift2import PackagePlugin3import Foundation4 5@main6struct CodeGenPlugin: BuildToolPlugin {7 func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {8 // Make sure it's a source target9 guard let sourceTarget = target as? SourceModuleTarget else {10 return []11 }12 13 // Find input files (with .codegen.json extension)14 let inputFiles = sourceTarget.sourceFiles.filter { file in15 file.path.extension == "json" && file.path.stem.hasSuffix(".codegen")16 }17 18 // Create a build command for each file19 return inputFiles.map { inputFile in20 let outputName = inputFile.path.stem21 .replacingOccurrences(of: ".codegen", with: "")22 let outputPath = context.pluginWorkDirectory23 .appending("Generated_\(outputName).swift")24 25 return .buildCommand(26 displayName: "CodeGen: \(outputName)",27 executable: try! context.tool(named: "codegen-tool").path,28 arguments: [29 "--input", inputFile.path.string,30 "--output", outputPath.string31 ],32 inputFiles: [inputFile.path],33 outputFiles: [outputPath]34 )35 }36 }37}Pre-Build vs Build Command
Build Tool Plugins can return two types of commands:
- buildCommand: Runs when input files change (incremental)
- prebuildCommand: Runs before every build (every time)
1// Prebuild Command example - runs on every build2func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {3 let outputDir = context.pluginWorkDirectory.appending("Generated")4 5 return [6 .prebuildCommand(7 displayName: "Generate Build Info",8 executable: try context.tool(named: "build-info-generator").path,9 arguments: ["--output-dir", outputDir.string],10 outputFilesDirectory: outputDir11 )12 ]13}Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
4. Creating a Command Plugin
Command Plugins are triggered manually and have broader permissions over the project.
1// Plugins/LintPlugin/LintPlugin.swift2import PackagePlugin3 4@main5struct LintPlugin: CommandPlugin {6 func performCommand(7 context: PluginContext,8 arguments: [String]9 ) async throws {10 // Find the SwiftLint tool11 let swiftLint = try context.tool(named: "swiftlint")12 13 // Determine the targets14 var argExtractor = ArgumentExtractor(arguments)15 let targetNames = argExtractor.extractOption(named: "target")16 17 let targets: [Target]18 if targetNames.isEmpty {19 targets = context.package.targets20 } else {21 targets = try context.package.targets(named: targetNames)22 }23 24 // Run lint for each target25 for target in targets {26 guard let sourceTarget = target as? SourceModuleTarget else {27 continue28 }29 30 let process = Process()31 process.executableURL = URL(fileURLWithPath: swiftLint.path.string)32 process.arguments = [33 "lint",34 "--path", sourceTarget.directory.string,35 "--reporter", "emoji"36 ]37 38 try process.run()39 process.waitUntilExit()40 41 if process.terminationStatus != 0 {42 Diagnostics.warning("SwiftLint found warnings for \(target.name)")43 }44 }45 }46}5. Real-World Examples
Example 1: Mock Generator Plugin
Automatic mock generation for your test files:
1// Plugins/MockGenPlugin/MockGenPlugin.swift2import PackagePlugin3 4@main5struct MockGenPlugin: BuildToolPlugin {6 func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {7 guard let target = target as? SourceModuleTarget else { return [] }8 9 // Find protocol files10 let protocolFiles = target.sourceFiles.filter { file in11 file.path.extension == "swift"12 }13 14 let outputDir = context.pluginWorkDirectory.appending("Mocks")15 16 return [17 .prebuildCommand(18 displayName: "Generate Mocks for \(target.name)",19 executable: try context.tool(named: "mockgen").path,20 arguments: [21 "--sources", target.directory.string,22 "--output", outputDir.string,23 "--testable-import", target.name24 ],25 outputFilesDirectory: outputDir26 )27 ]28 }29}Example 2: Asset Catalog Validator
Validate your assets during the build:
1struct AssetValidatorPlugin: BuildToolPlugin {2 func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {3 guard let target = target as? SourceModuleTarget else { return [] }4 5 // Find .xcassets directories6 let assetCatalogs = target.sourceFiles.filter { file in7 file.path.extension == "xcassets"8 }9 10 guard !assetCatalogs.isEmpty else { return [] }11 12 let reportPath = context.pluginWorkDirectory.appending("asset-report.txt")13 14 return [15 .prebuildCommand(16 displayName: "Validate Assets",17 executable: try context.tool(named: "asset-validator").path,18 arguments: assetCatalogs.map { item in item.path.string } +19 ["--output", reportPath.string],20 outputFilesDirectory: context.pluginWorkDirectory21 )22 ]23 }24}6. Plugin Security Model
SPM plugins run inside a sandbox. Understanding this security layer is critical.
Sandbox Rules
Permission | Build Tool | Command (default) | Command (extended) |
|---|---|---|---|
Reading sources | Target directory only | Target directory only | Entire project |
Writing files | Plugin work dir | None | Project directory |
Network access | None | None | None |
Running processes | Declared tools | Declared tools | Declared tools |
Environment variables | Restricted | Restricted | Restricted |
7. CI/CD Integration
Integrating your plugins into a CI/CD pipeline is straightforward:
1# GitHub Actions - using a Command plugin2swift package plugin --allow-writing-to-package-directory lint-code3swift package plugin --allow-writing-to-package-directory format-code --target MyApp4 5# Build tool plugins run automatically6swift build7swift testGOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Reader Reward
Congratulations! Since you read this post all the way through, I have something special for you:
Conclusion and Recommendations
Swift Package Plugins are the most effective way to modernize your build process. Ditch manual scripts, automate repetitive tasks, and boost your team's productivity. Generate code with Build Tool Plugins, and standardize linting and formatting with Command Plugins. Don't forget to publish your plugins as separate packages to share them with the community.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

