SECRET OF CSS

Create Your First Swift Package Command Plugin | by Riccardo Cipolleschi | Sep, 2022


How to write and debug a Swift Package Command plugin

0*0OQ7lv4opjGGqPlp
Photo by Jess Bailey on Unsplash

This year, Apple released a new feature for the Swift Package Manager: Swift Package Plugins. We can now write two kinds of plugins that are automatically integrated with Xcode:

  • Build (and pre-build) Plugins.
  • Command Plugins.

I already talked about building plugins in a couple of articles: “Implement Your First Swift Package Build Plugin” and “How to Use Xcode Plugins in Your iOS App”.

Today, I want to share with you which steps are required to create a Command Plugin. The developer experience for these plugins is not great, so I want to share also a technique to debug them.

You can find more information about Swift Package Plugins in these WWDC22 videos: Meet Swift Package Plugins, Create Swift Package Plugins

In this article, you are going to create a Command Plugin that generates some Swift code from a JSON specification.

To create a Command plugin, you need various pieces:

  • A Package.swift file to define the structure of the Plugin.
  • A proper folder structure.
  • A Swift struct that conforms to the CommandPlugin protocol.
  • The business logic to implement the plugin.
  • Optionally, an additional Package to test the plugin.

The Package.swift

Swift Package Plugins are defined as Swift Packages. You need to create a new package for your plugin.

The typical package structure for a Command Plugin is the following:

The important details to notice are:

  • The swift-tool-version at line 1 must be at least 5.7.
  • The target is of type .plugin with the .command capability.

To completely define a command plugin, we need to add a couple more properties: an intent and a set of permissions.

The intent is the reason why the plugin exists. It is structured with a verb and a description.

  • Commands plugins can be also invoked by the swift package command line tool. The verb property is the argument we can use in the command line to invoke this Command plugin. The syntax is the following:
swift package plugin <verb> [args...]
  • The description property is a human-readable description of the plugin.

The set of permissions comes from an enum defined in the Package API. The enum has no explicit cases, but it offers a single static function: the writeToPackageDirectory. This tells Xcode that your plugin needs write access and the IDE will prompt a message to the user whenever the command is invoked. The prompt will show the reason String: a human readable description that explains to your users what the plugin will do with this permission.

The Folder Structure

Like all the Swift packages, Plugins needs to adhere to a proper folder structure to be properly built. You can customize the structure in the Package.swift by using the path property, but the default folder structure is this one:

CodeGeneratorPlugin
├── Package.swift
└── Plugins
└── CodeGenerator
└── CodeGenerator.swift

The CodeGeneratorPlugin is the folder that contains the current package. The Plugins folder is the home for all the Plugins defined in the Package.swift. The code of each plugin must be located in a folder with the same name of the plugin, CodeGenerator in this example.

The CodeGenerator.swift file is the entry point for the plugin and it will contain its business logic. Differently from the other folders, it is not necessary to call it CodeGenerator: the Swift file can have any name.

The CodeGenerator.swift

This struct is the entry point of the plugin. The basic structure is something like this:

The important fragments are:

  1. The import PackagePlugin statement. It imports the framework with all the new APIs for the plugins.
  2. The @main annotation. It defines the entry point for the plugin.
  3. The conformance to CommandPlugin. It marks the struct as a proper Command plugin and it forces to implement its methods.
  4. The performCommand method. It contains the logic of the plugin.

The performCommand has two parameters: the context and the arguments. The context can be used to read information from the package, like its path. The arguments are a list of arguments that can be passed to the commands from Xcode or from the command line.

The logic to generate code

This is the last step to create the plugin. You need to write the code that generates Swift code starting from the JSON specifications. To achieve that, you may need some helper classes and some functions.

The JSON Specification

The first step is to define the data that represents your JSON entities. In this example, you want to generate some Swift structs. These data structs are very simple: they only have let properties.

The JSON you want to parse has this structure:

{
"fields": [{
"label": "<variable name>",
"type": "<variable type>",
["subtype": "<variable type>"]
}]
}

This is a JSON object that represents a single struct. It has a fields property which contains another object that completely defines a Swift property. The label is the name of the property in the struct; the type is the main type of the property. In case of generics, it needs a subtype to specify the type of the generic.

The name of the struct will be the name of the file. So, a valid JSON would be the following Person.json file:

{
"fields": [
{
"label": "name",
"type": "String",
},
{
"label": "surname",
"type": "String",
},
{
"label": "age",
"type": "Int",
},
{
"label": "family",
"type": "Array",
"subtype": "Person"
}
]
}

This Person type has a name, a surname, an age and a family, which is a list of other Person types. After the execution of the plugin, you expect to obtain the following swift struct:

struct Person {
let name: String,
let surname: String,
let age: Int
let family: [Person]
}

The Data Model

To properly handle this JSON in the plugin logic, you need to model it properly, so that you can decode it.

To achieve that, you need these two structs:

The first struct is a wrapper to contain the list of fields. It represents the top-level JSON object.

The Field struct is the data model that defines the inner objects. It has a property for the label, a property for the type, and an optional property for the subtype, in case you have to deal with a generic.

The logic

Finally, we can implement the logic. You can split it in various functions within the plugin itself to simplify it.

The first function is the performCommand, the entry point of the plugin:

You can look at it as the Composition Root of your plugin: you can fetch all the relevant data from the context, instantiate the dependencies, and pass them to the rest of the code.

The performCommand invokes the executeCommand:

This methods extracts all the structs that need to be generated, by using the drillDown method. If there are no structs, it returns.

Then, it writes the structs to a file called Struct.swift: all the structs will be contained in a single file, for simplicity’s sake.

The drillDown method is responsible to crawls the folder structure of the package to look for JSON specifications, in a recursive way:

This example works under the assumption that all the JSON specifications are located in a folder called Definitions.

The drillDown method starts by getting the content of the directory property which, by default, is the Package main folder. Then, if the last path component of the directory is Definitions, it retrieve the full path of every item contained in the folder and, for each of them, it invokes the createSwiftStruct function.

Otherwise, it proceeds crawling the tree: for each item in the current folder, it check whether it is a folder or not. If it is a folder, it tries to drillDown into it and to accumulate the result in a variable that will be returned at the end of the recursion.

The last method is the createSwiftStructure:

This method reads the content of the file passed as parameter. It then tries to decode it using the Data model defined above.

Then, it extracts the struct name from the file name and it creates the list of fields.

Finally, it returns a String that is a valid Swift struct.

How to Use It

Now, it’s time to try your command plugin in another package.

First, create a new package in the Package.swift. To do so, you can just add a .target to the Package.swift file:

The package also require a proper folder structure. It should be something like this:

CodeGeneratorPlugin
├── Package.swift
└── Plugins
│ └── CodeGenerator
│ └── CodeGenerator.swift
└── Sources
└── MyCode
└── HelloWorld.swift

HelloWorld.swift is just an empty Swift file: every Swift package must have at least a Swift file into its folder.

At this point, if you right-click on the CodeGeneratorPlugin project, you can already see that Xcode shows your CodeGenerator custom plugin in the contextual menu!

1*aWwefmJ 0BYywhfXQ sSPw

The next step is to add a Definitions folder with thePerson.json file that we described above.

Once that’s done, by clicking on the CodeGenerator menu item in the contextual menu, Xcode will present a dialog to let you:

  1. Choose on which target the plugin should run.
  2. Pass additional arguments, if they are needed.
1*BwS Gk 3IPw3vpVX4BzopA

In this case, we don’t need any extra argument and we can safely click on Run.

Now, Xcode asks if it is allowed to run the Command.

1*vEyRbBKdtZL8H3vDZGF9QQ

The line that starts with From the author of this dialog shows the reason you set up in the Package.swift for your plugin in the first step of this article.

By clicking on the Allow Command to Change Files, Xcode will execute the command. After a few seconds, you should see a Structs.swift file appear below the HelloWorld.swift.

The new file should have this content:

Congratulations! You created your first command plugin and you applied it to another target using Xcode.

Unfortunately, nobody is able to write perfect code without a bit of trial and error. While developing this plugin, I run it frequently to see if it was working, but the developer experience was very frustrating:

  1. At the beginning, the plugin was not generating any output because it was failing.
  2. Xcode was not emitting any error I can act on.
  3. It was not possible to attach the debugger to see what was going on.
  4. The Swift print function was not writing anything anywhere.

My solution to debug this plugin was to write every step into a log file. To achieve that I followed these steps:

  1. I created a global var log: [String]variable. This is recreated every time the command is run, so there is no problem of memory sharing between processes.
  2. I created a log(_ message: String) function to append messages into the log variable.
  3. I created a printLog() function to join all the logs and to write them to a logs file I can inspect after the execution.
  4. Finally, I instrumented my code with calls to the log(_:) function to see what was going on.

Note: this is a quick implementation and a non efficient one. It recreates the file every time, rewriting the whole content. A better solution would have been to append the new logs to an existing file.

With this trick, I was able to log the various errors I incurred into and to successfully implement the plugin.

In today’s article, you learned how to configure a Command Plugin for Swift 5.7.

You learned how to structure the Package and the basic concepts to implement it. You also learned how to run it in Xcode. Due to the poor developer experience, you learned how to create a basic logger solution to see the various execution steps.

Command plugins are quite useful, but they would be even more useful when used from the command line: we would be able to integrate custom Commands in our Continuous Integration environments, for example.

I’m looking forward to seeing what the community will create with these new powerful tools!



News Credit

%d bloggers like this: