Streaming is available in most browsers,
and in the Developer app.
-
Meet Swift Package plugins
Discover how you can perform actions on Swift packages and Xcode projects with Swift package plugins. We'll go over how these plugins work and explore how you can use them to generate source code and automate your development workflow.
Resources
Related Videos
WWDC23
WWDC22
WWDC19
-
Download
♪ Mellow instrumental hip hop music ♪ ♪ Hello, my name is Anders. In this video, I'll show you how to get started with Swift package plugins. Swift Packages were introduced in Xcode 11. They provide a great way of distributing libraries as source code. Xcode 14 extends this approach to your development workflow, letting you use plugins to do things like generating source code during a build, or automating your release tasks. We'll start by taking a look at what package plugins are and how they work, and then talk in more detail about the two kinds of package plugins that Xcode 14 supports: command plugins and build tool plugins. So first of all, what is a plugin? A package plugin is a Swift script that can perform actions on a Swift package or an Xcode project. A plugin uses API that Xcode provides especially for this purpose. Package plugins are implemented as Swift packages. A package can provide plugins together with libraries and executables, or a package could focus only on providing plugins. A package plugin can be implemented using more than one source file, and a Swift package can define more than one plugin. A highly specialized plugin can be private to the package that provides it, and in that case, it's available only within that package. But a general-purpose plugin can be made available to other packages by defining it as a package product. That lets other packages use it too, in a way that's similar to how a package can use a library from another package. But unlike a library, a dependency on a plugin does not bring in runtime content into your app. Instead, it lets you access development tools that run on your own machine or in your build automation. So what can a package plugin do? Well, in Xcode 14 there are two kinds of package plugins: command plugins and build tool plugins. Command plugins implement custom actions that you can run whenever you want to. They can run source code formatters or linters, or they can perform other tasks as part of your development workflow. That might include updating contributor lists or copyright dates in source files based on Git history, or other things you might have arbitrary scripts to do today. If it needs to, a command plugin can ask for permission to modify the files in a package. And that's especially useful for code formatting. Not all command plugins need write permission. Some commands could create reports or calculate metrics about your code, without needing to make any changes. Build tool plugins extend the build system's dependency graph. They're particularly useful for generating source code or resources as part of a build. Unlike command plugins, which are invoked for a whole package or a project at a time, build tool plugins are applied to each target that needs them. Let's take a look at using a command plugin in Xcode. Here's a little iOS app that shows various kinds of geometric shapes. It's composed of an app project and a local package. The package implements a library that provides the core data types and logic for the app. I'm thinking of splitting out the package into its own repository so that others can use it, and as part of this, I'd like to create a contributor file that lists everyone who has committed code of this package. I could write a custom script to do this. But I know of a package that provides some useful plugins for working with code, and I think it has a plugin that does exactly what I want. To get access to those plugins, I'll do the same thing as if I needed a library from another package: I'm going to add a package dependency in the manifest of my local package. When I save the manifest, Xcode fetches the remote package and it appears in the Package Dependencies section. I notice that Xcode has also fetched SwiftFormat, which is a popular tool for formatting code. This is because one of the command plugins in the utility package in turn has a dependency on SwiftFormat. Now that I've added this dependency, I have access to any plugin commands that the package provides. I use the context menu on the package I want to apply the command to. Now there are three new commands in the menu; one is for reformatting source code using SwiftFormat, and two others provide specialized actions. One of them generates or updates contributor lists based on the commit history in Git, and another updates the copyright dates in my source files. The command in the middle there does exactly what I want. When I invoke the plugin command on my package, Xcode lets me choose which of its targets to pass to the plugin. In this case, I'm going to invoke it on the whole package. And if the plugin takes custom arguments, I can pass those here as well. I click Run, and because the plugin is going to modify the file system, Xcode warns me about that. I can see the plugin author's stated reason for wanting to modify my code, but I want to take a peek at the implementation of the plugin as well. So I choose Show Command, and Xcode takes me to the code.
What this plugin is doing is safe, so I'm going to invoke the command again and this time, I will choose Run.
I'll tell Xcode to remember my choice for this plugin. This particular plugin uses Git history to generate a file listing showing the names of contributors, but there's a lot of flexibility in what command plugins can do. Now that we've used a command plugin in Xcode, let's take a closer look at how plugins work under the hood. Package plugins are Swift scripts that are compiled and run when they are needed. Each plugin runs as a separate process. Plugins have access to a distilled representation of the input package, including its source files. A plugin also gets information about any dependencies of the package. Many plugins call command-line tools as part of doing their work. Plugins can also create files and directories, and can perform other actions using standard libraries such as Foundation. A plugin runs in a sandbox that prevents network access and that only allows writing to a few places in the file system, such as the build outputs directory. But command plugins can ask for permission to also modify files in the package source directory. If the user approves, the sandbox is configured to allow writing to those locations. The plugin can also send results back to Xcode. It can emit warnings and errors, and build tool plugins can define tool invocations for Xcode to run during the build. All package plugins use API from the PackagePlugin module provided by Xcode. This API allows the plugin to access the input package, and if appropriate, to return results to Xcode. The main source file that implements the plugin also defines the main entry point. This should be a class or a struct that conforms to the protocol that matches the type of plugin. The specific entry point function that Xcode calls depends on what kind of plugin it is. You can learn more about the PackagePlugin API in the "Create Swift Package plugins" video. Earlier, we used a command plugin to make changes to our package. Let's look at some more of the specifics of command plugins. Command plugins extend the development workflow. They are applied directly to a package, not during a build. Not all command plugins modify the file system -- there are useful actions that don't involve changing any files. But if a command does want to write to the file system, it must declare that in the manifest of the package that implements the plugin. This causes Xcode to ask the user for permission before letting the plugin run. Plugins are usually quite small, and often depend on other tools to do the actual work. Earlier, we saw that one of the plugins uses SwiftFormat for all the real work. Dependencies on tool packages can be either binaries or source code -- Xcode will build any required tools from source before the command is invoked. Note that the plugin can be provided by a different package than the tool it relies on. In the implementation of command plugins, the main type conforms to the CommandPlugin protocol, and the plugin implements the performCommand entry point. This entry point takes a context and any custom arguments provided by the user. Let's look at a different way of invoking command plugins. I'm going to use the same project as before, and because I added the dependency on the SourceCodeUtilities package earlier, I can invoke the same plugins in Terminal. First I'm going to change directory into the CoreLibs package, since that's the package that I want to apply the command plugin to. Swift Package Manager 5.6 has a new subcommand for plugins. I'll type "swift package plugin --List" to see what plugins are available. This shows the same plugins as in the menu in Xcode. Here on the command line, each command also shows the verb that should be used to run it. I'll use the verb for regenerating a contributor list, as I did in Xcode. This plugin wants permission to write to the file system, since it's going to create a file. I type "yes" to allow this, and the plugin can run and update the contributor list. I could also have used a package manager option that allows the plugin to write to the file system without asking. This is particularly useful if you're invoking it from a CI system or other build automation. But be sure you know what the plugin is doing before using that option. Just like in Xcode, I can pass command line arguments to the plugin. Any arguments after the plugin's action verb will be passed to the plugin. In this case, I pass a verbose flag to see more output from the plugin as it runs. Each command plugin defines what arguments it supports. Until now, we've been talking mostly about command plugins. But there are a few more things to say about build tool plugins. Unlike a command plugin, a build tool plugin does not do its work immediately. Instead, it creates and returns build tool invocations for Xcode to run later when the package is built. Each of those tool invocations has a command line to run, and it also has inputs and outputs that tells Xcode when to run it. Build tool plugins can define commands that run during the build or before the build. We'll take a look at the difference in a minute.
Commands returned by build tool plugins are usually configured to write their outputs to the build directory, so they persist between incremental builds. And like the plugins themselves, the commands defined by a build tool plugin run in a sandbox that prevents network access and any changes to the package. In the implementation of a build tool plugin, the main type conforms to the BuildToolPlugin protocol and the plugin implements the createBuildCommands entry point. This entry point takes a context and the target to create build commands for. It returns any custom build commands that should run when the package is built. There are two basic kinds of build commands that a build tool plugin can return. Ordinary build commands specify input and output paths, and only run when the outputs are missing or the inputs have changed. Prebuild commands run before the build starts, and can be used when the names of the outputs are unknown ahead of time. Prebuild commands run before every build, so they should make sure to do as little work as possible when there are no changes. Build commands and prebuild commands are great for generating source code or resources. So how does Xcode know which build tool plugins to apply to a package target? In SwiftPM 5.6 and later, there is a new plugins parameter in the package manifest that lists the build tool plugins that a target wants. This parameter specifies any build tool plugins needed by the target, and just as with any runtime libraries it depends on, those plugins can be either in the same package or in another package. Let's go back to Xcode. I'm going to configure my geometry app to use a build tool plugin. In this particular case, I have a custom command line tool that generates Swift code based on some data files in my Core Library target. The specific details aren't important, but what I want to end up with are generated type-safe Swift accessors for each piece of data. In addition to my data files, I've been using a custom tool to generate source code that I've checked into my repository. I have been manually running this tool to regenerate Swift wrapper code and committing the changes whenever my data files change. But with the build tool plugin, I can do better. I can generate the code during the build and avoid having to keep the generated code in my repository.
To get access to the plugin, I go to the package manifest and add a dependency on the package that provides the source generator plugin I want to use.
The targets in my package now have access to any build tool plugins defined in that package.
Now I go to the target that needs to use the plugin, and I add a plugins parameter to its definition.
This tells Xcode that it wants to apply a particular build tool from that package to my target. Now I can go and delete those generated source files from my repository. They'll be created or updated as needed during the build.
There, that's much cleaner. And now when I build and run my app, my build tool plugin tells Xcode to invoke my code-generation tool whenever my data files change. The generated code will be stored along with the other build files in my build folder, keeping my repository clean. In this video, we've talked about what Swift package plugins are and how they work. We have discussed some of the similarities and differences between command plugins and build tool plugins. Both types of plugins let you replace a variety of random scripts with a more structured kind of extensibility in your packages. Build tool plugins let you extend the build system to generate sources and resources, or to do other custom work as part of your build. Command plugins let you automate common development tasks with custom actions. They might be tailored to a particular workflow or could be written to be useful in a wide variety of cases. To learn how to create your own package plugins, be sure to check out the "Create Swift Package plugins" video. Thanks for watching and enjoy the rest of WWDC 2022. ♪
-
-
import PackagePlugin @main struct MyPlugin: ... { // Entry points specific to plugin capability. These entry points are invoked // when the plugin is applied to a package. } #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin extension MyPlugin: ... { // Entry points specific to plugin capability. These entry points are invoked // when the plugin is applied to an Xcdeo project. } #endif
-
8:33 - Structure of a command plugin with conditional support for Xcode projects when running in Xcode
import PackagePlugin @main struct MyPlugin: CommandPlugin { /// This entry point is called when operating on a Swift package. func performCommand(context: PluginContext, arguments: [String]) throws { debugPrint(context) } } #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin extension MyPlugin: XcodeCommandPlugin { /// This entry point is called when operating on an Xcode project. func performCommand(context: XcodePluginContext, arguments: [String]) throws { debugPrint(context) } } #endif
-
11:13 - Structure of a build tool plugin with conditional support for Xcode projects when running in Xcode
import PackagePlugin @main struct MyPlugin: BuildToolPlugin { /// This entry point is called when operating on a Swift package. func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] debugPrint(context) return [] } } #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin extension MyPlugin: XcodeBuildToolPlugin { /// This entry point is called when operating on an Xcode project. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] debugPrint(context) return [] } } #endif
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.