Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2022 Developer ToolsSwift

WWDC22 · 24 min · Developer Tools / Swift

Create Swift Package plugins

Tailor your development workflow and learn how to write your own package plugins in Swift. We’ll show you how you can extend Xcode’s functionality by using the PackagePlugin API to generate source code or automate release tasks and share best practices for creating great plugins.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 14 snippets

GenerateContributors plugin target swift · at 3:40 ↗
// MARK: Plugins

        .plugin(
            name: "GenerateContributors",
            capability: .command(
                intent: .custom(verb: "regenerate-contributors-list",
                                description: "Generates the CONTRIBUTORS.txt file based on Git logs"),
                permissions: [
                    .writeToPackageDirectory(reason: "This command write the new CONTRIBUTORS.txt to the source root.")
                ]
            )),
GenerateContributors plugin implementation swift · at 5:06 ↗
import PackagePlugin
import Foundation

@main
struct GenerateContributors: CommandPlugin {

    func performCommand(
        context: PluginContext,
        arguments: [String]
    ) async throws {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
        process.arguments = ["log", "--pretty=format:- %an <%ae>%n"]

        let outputPipe = Pipe()
        process.standardOutput = outputPipe
        try process.run()
        process.waitUntilExit()

        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: outputData, as: UTF8.self)

        let contributors = Set(output.components(separatedBy: CharacterSet.newlines)).sorted().filter { !$0.isEmpty }
        try contributors.joined(separator: "\n").write(toFile: "CONTRIBUTORS.txt", atomically: true, encoding: .utf8)
    }
}
Minimum Deployment Target swift · at 10:28 ↗
platforms: [
            .macOS("10.15"),
            .iOS("12.0"),
            .tvOS("12.0"),
            .watchOS("6.0"),
        ],
Basic SwiftUI view and preview swift · at 10:35 ↗
import SwiftUI

struct ContentView: View {
    var body: some View {
        Image("Xcode", bundle: .module)
            .resizable()
            .frame(width: 200.0, height: 200.0)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
AssetConstantsExec executable target swift · at 14:56 ↗
.executableTarget(name: "AssetConstantsExec"),
AssetConstantsExec implementation swift · at 15:03 ↗
import Foundation

let arguments = ProcessInfo().arguments
if arguments.count &lt; 3 {
    print("missing arguments")
}
let (input, output) = (arguments[1], arguments[2])

struct Contents: Decodable {
    let images: [Image]
}

struct Image: Decodable {
    let filename: String?
}

var generatedCode = """
    import Foundation
    import SwiftUI
    
    """

try FileManager.default.contentsOfDirectory(atPath: input).forEach { dirent in
    guard dirent.hasSuffix("imageset") else {
        return
    }

    let contentsJsonURL = URL(fileURLWithPath: "\(input)/\(dirent)/Contents.json")
    let jsonData = try Data(contentsOf: contentsJsonURL)
    let asset🐱alogContents = try JSONDecoder().decode(Contents.self, from: jsonData)
    let hasImage = asset🐱alogContents.images.filter { $0.filename != nil }.isEmpty == false

    if hasImage {
        let basename = contentsJsonURL.deletingLastPathComponent().deletingPathExtension().lastPathComponent
        generatedCode.append("public let \(basename) = Image(\"\(basename)\", bundle: .module)\n")
    }
}

try generatedCode.write(to: URL(fileURLWithPath: output), atomically: true, encoding: .utf8)
AssetConstantsExec plugin target swift · at 15:48 ↗
.plugin(name: "AssetConstants", capability: .buildTool(), dependencies: ["AssetConstantsExec"]),
AssetConstantsExec plugin implementation swift · at 16:12 ↗
guard let target = target as? SourceModuleTarget else {
                    return []
                }

        return try target.sourceFiles(withSuffix: "xcassets").map { asset🐱alog in
                    let base = asset🐱alog.path.stem
                    let input = asset🐱alog.path
                    let output = context.pluginWorkDirectory.appending(["\(base).swift"])

                    return .buildCommand(displayName: "Generating constants for \(base)",
                                         executable: try context.tool(named: "AssetConstantsExec").path,
                                         arguments: [input.string, output.string],
                                         inputFiles: [input],
                                         outputFiles: [output])
                }
GenstringsPlugin target swift · at 20:19 ↗
.plugin(name: "GenstringsPlugin", capability: .buildTool()),
GenstringsPlugin product swift · at 20:26 ↗
.plugin(name: "GenstringsPlugin", targets: ["GenstringsPlugin"]),
GenstringsPlugin implementation swift · at 20:44 ↗
guard let target = target as? SourceModuleTarget else {
                    return []
                }

        let resourcesDirectoryPath = context.pluginWorkDirectory
                    .appending(subpath: target.name)
                    .appending(subpath: "Resources")
                let localizationDirectoryPath = resourcesDirectoryPath
                    .appending(subpath: "Base.lproj")

                try FileManager.default.createDirectory(atPath: localizationDirectoryPath.string, withIntermediateDirectories: true)

        let swiftSourceFiles = target.sourceFiles(withSuffix: ".swift")
                let inputFiles = swiftSourceFiles.map(\.path)

        return [
                    .prebuildCommand(
                        displayName: "Generating localized strings from source files",
                        executable: .init("/usr/bin/xcrun"),
                        arguments: [
                            "genstrings",
                            "-SwiftUI",
                            "-o", localizationDirectoryPath
                        ] + inputFiles,
                        outputFilesDirectory: localizationDirectoryPath
                    )
                ]
Localized string API swift · at 21:10 ↗
import Foundation

public func GetLocalizedString() -&gt; String {
    return NSLocalizedString("World", comment: "A comment about the localizable string")
}
Path-based dependency on GenstringsPlugin swift · at 21:44 ↗
.package(path: "../GenstringsPlugin"),
Use of GenstringsPlugin in library target swift · at 21:52 ↗
plugins: [ .plugin(name: "GenstringsPlugin", package: "GenstringsPlugin"), ]