Home IOS Development Newbie’s information to Swift bundle supervisor command plugins

Newbie’s information to Swift bundle supervisor command plugins

0
Newbie’s information to Swift bundle supervisor command plugins

[ad_1]

Introduction to Swift Bundle Supervisor plugins

To start with I would like to speak a couple of phrases concerning the new SPM plugin infrastructure, that was launched within the Swift 5.6 launch. The very first proposal describes the detailed design of the plugin API with some plugin examples, that are fairly useful. Truthfully talking I used to be a bit to lazy to rigorously learn by way of all the documentation, it is fairly lengthy, however lengthy story quick, you’ll be able to create the next plugin varieties with the at the moment current APIs:

  • Construct instruments – may be invoked through the SPM targets
    • pre-build – runs earlier than the construct begins
    • construct – runs throughout the construct
  • Instructions – may be invoked through the command line
    • supply code formatting – modifies the code inside bundle
    • documentation era – generate docs for the bundle
    • customized – person outlined intentions

For the sake of simplicity on this tutorial I am solely going to write down a bit concerning the second class, aka. the command plugins. These plugins had been a bit extra attention-grabbing for me, as a result of I needed to combine my deployment workflow into SPM, so I began to experiment with the plugin API to see how arduous it’s to construct such a factor. Seems it is fairly simple, however the developer expertise it is not that good. 😅

Constructing a supply code formatting plugin

The very very first thing I needed to combine with SPM was SwiftLint, since I used to be not capable of finding a plugin implementation that I may use I began from scratch. As a place to begin I used to be utilizing the instance code from the Bundle Supervisor Command Plugins proposal.

mkdir Instance
cd Instance
swift bundle init --type=library

I began with a model new bundle, utilizing the swift bundle init command, then I modified the Bundle.swift file in accordance with the documentation. I’ve additionally added SwiftLint as a bundle dependency so SPM can obtain & construct the and hopefully my customized plugin command can invoke the swiftlint executable when it’s wanted.


import PackageDescription

let bundle = Bundle(
    title: "Instance",
    platforms: [
        .macOS(.v10_15),
    ],
    merchandise: [
        .library(name: "Example", targets: ["Example"]),
        .plugin(title: "MyCommandPlugin", targets: ["MyCommandPlugin"]),
    ],
    dependencies: [
        .package(url: "https://github.com/realm/SwiftLint", branch: "master"),
    ],
    targets: [
        .target(name: "Example", dependencies: []),
        .testTarget(title: "ExampleTests", dependencies: ["Example"]),
       
        .plugin(title: "MyCommandPlugin",
                functionality: .command(
                    intent: .sourceCodeFormatting(),
                    permissions: [
                        .writeToPackageDirectory(reason: "This command reformats source files")
                    ]
                ),
                dependencies: [
                    .product(name: "swiftlint", package: "SwiftLint"),
                ]),
    ]
)

I’ve created a Plugins listing with a fundamental.swift file proper subsequent to the Sources folder, with the next contents.

import PackagePlugin
import Basis

@fundamental
struct MyCommandPlugin: CommandPlugin {
    
    func performCommand(context: PluginContext, arguments: [String]) throws {
        let software = strive context.software(named: "swiftlint")
        let toolUrl = URL(fileURLWithPath: software.path.string)
        
        for goal in context.bundle.targets {
            guard let goal = goal as? SourceModuleTarget else { proceed }

            let course of = Course of()
            course of.executableURL = toolUrl
            course of.arguments = [
                "(target.directory)",
                "--fix",
               
            ]

            strive course of.run()
            course of.waitUntilExit()
            
            if course of.terminationReason == .exit && course of.terminationStatus == 0 {
                print("Formatted the supply code in (goal.listing).")
            }
            else {
                let downside = "(course of.terminationReason):(course of.terminationStatus)"
                Diagnostics.error("swift-format invocation failed: (downside)")
            }
        }
    }
}

The snippet above ought to find the swiftlint software utilizing the plugins context then it’s going to iterate by way of the accessible bundle targets, filter out non source-module targets and format solely these targets that comprises precise Swift supply recordsdata. The method object ought to merely invoke the underlying software, we are able to wait till the kid (swiftlint invocation) course of exists and hopefully we’re good to go. 🤞

Replace: kalKarmaDev advised me that it’s attainable to move the --in-process-sourcekit argument to SwiftLint, this may repair the underlying problem and the supply recordsdata are literally fastened.

I needed to record the accessible plugins & run my supply code linter / formatter utilizing the next shell instructions, however sadly looks like the swiftlint invocation half failed for some unusual purpose.

swift bundle plugin --list
swift bundle format-source-code #will not work, wants entry to supply recordsdata
swift bundle --allow-writing-to-package-directory format-source-code

# error: swift-format invocation failed: NSTaskTerminationReason(rawValue: 2):5
# what the hell occurred? 🤔

Looks like there’s an issue with the exit code of the invoked swiftlint course of, so I eliminated the success test from the plugin supply to see if that is inflicting the problem or not additionally tried to print out the executable command to debug the underlying downside.

import PackagePlugin
import Basis

@fundamental
struct MyCommandPlugin: CommandPlugin {
    
    func performCommand(context: PluginContext, arguments: [String]) throws {
        let software = strive context.software(named: "swiftlint")
        let toolUrl = URL(fileURLWithPath: software.path.string)
        
        for goal in context.bundle.targets {
            guard let goal = goal as? SourceModuleTarget else { proceed }

            let course of = Course of()
            course of.executableURL = toolUrl
            course of.arguments = [
                "(target.directory)",
                "--fix",
            ]

            print(toolUrl.path, course of.arguments!.joined(separator: " "))

            strive course of.run()
            course of.waitUntilExit()
        }
    }
}

Deliberately made a small “mistake” within the Instance.swift supply file, so I can see if the swiftlint –fix command will remedy this problem or not. 🤔

public struct Instance {
    public personal(set) var textual content = "Good day, World!"

    public init() {
        let xxx :Int = 123
    }
}

Seems, after I run the plugin through the Course of invocation, nothing occurs, however after I enter the next code manually into the shell, it simply works.

/Customers/tib/Instance/.construct/arm64-apple-macosx/debug/swiftlint /Customers/tib/Instance/Assessments/Instance --fix
/Customers/tib/Instance/.construct/arm64-apple-macosx/debug/swiftlint /Customers/tib/Instance/Assessments/ExampleTests --fix

All proper, so we positively have an issue right here… I attempted to get the usual output message and error message from the operating course of, looks like swiftlint runs, however one thing within the SPM infrastructure blocks the code modifications within the bundle. After a number of hours of debugging I made a decision to present a shot to swift-format, as a result of that is what the official docs counsel. 🤷‍♂️


import PackageDescription

let bundle = Bundle(
    title: "Instance",
    platforms: [
        .macOS(.v10_15),
    ],
    merchandise: [
        .library(name: "Example", targets: ["Example"]),
        .plugin(title: "MyCommandPlugin", targets: ["MyCommandPlugin"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-format", exact: "0.50600.1"),
    ],
    targets: [
        .target(name: "Example", dependencies: []),
        .testTarget(title: "ExampleTests", dependencies: ["Example"]),
       
        .plugin(title: "MyCommandPlugin",
                functionality: .command(
                    intent: .sourceCodeFormatting(),
                    permissions: [
                        .writeToPackageDirectory(reason: "This command reformats source files")
                    ]
                ),
                dependencies: [
                    .product(name: "swift-format", package: "swift-format"),
                ]),
    ]
)

Modified each the Bundle.swift file and the plugin supply code, to make it work with swift-format.

import PackagePlugin
import Basis

@fundamental
struct MyCommandPlugin: CommandPlugin {
    
    func performCommand(context: PluginContext, arguments: [String]) throws {
        let swiftFormatTool = strive context.software(named: "swift-format")
        let swiftFormatExec = URL(fileURLWithPath: swiftFormatTool.path.string)

        
        for goal in context.bundle.targets {
            guard let goal = goal as? SourceModuleTarget else { proceed }

            let course of = Course of()
            course of.executableURL = swiftFormatExec
            course of.arguments = [

                "--in-place",
                "--recursive",
                "(target.directory)",
            ]
            strive course of.run()
            course of.waitUntilExit()

            if course of.terminationReason == .exit && course of.terminationStatus == 0 {
                print("Formatted the supply code in (goal.listing).")
            }
            else {
                let downside = "(course of.terminationReason):(course of.terminationStatus)"
                Diagnostics.error("swift-format invocation failed: (downside)")
            }
        }
    }
}

I attempted to run once more the very same bundle plugin command to format my supply recordsdata, however this time swift-format was doing the code formatting as an alternative of swiftlint.

swift bundle --allow-writing-to-package-directory format-source-code
// ... loading dependencies
Construct full! (6.38s)
Formatted the supply code in /Customers/tib/Linter/Assessments/ExampleTests.
Formatted the supply code in /Customers/tib/Linter/Sources/Instance.

Labored like a attraction, my Instance.swift file was fastened and the : was on the left facet… 🎊

public struct Instance {
    public personal(set) var textual content = "Good day, World!"

    public init() {
        let xxx: Int = 123
    }
}

Yeah, I’ve made some progress, however it took me various time to debug this problem and I do not like the truth that I’ve to fiddle with processes to invoke different instruments… my intestine tells me that SwiftLint just isn’t following the usual shell exit standing codes and that is inflicting some points, possibly it is spawning youngster processes and that is the issue, I actually do not know however I do not needed to waste extra time on this problem, however I needed to maneuver ahead with the opposite class. 😅

Integrating the DocC plugin with SPM

As a primary step I added some dummy feedback to my Instance library to have the ability to see one thing within the generated documentation, nothing fancy just a few one-liners. 📖


public struct Instance {

    
    public personal(set) var textual content = "Good day, World!"
    
    
    public init() {
        let xxx: Int = 123
    }
}

I found that Apple has an official DocC plugin, so I added it as a dependency to my undertaking.


import PackageDescription

let bundle = Bundle(
    title: "Instance",
    platforms: [
        .macOS(.v10_15),
    ],
    merchandise: [
        .library(name: "Example", targets: ["Example"]),
        .plugin(title: "MyCommandPlugin", targets: ["MyCommandPlugin"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-format", exact: "0.50600.1"),
        .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

    ],
    targets: [
        .target(name: "Example", dependencies: []),
        .testTarget(title: "ExampleTests", dependencies: ["Example"]),
       
        .plugin(title: "MyCommandPlugin",
                functionality: .command(
                    intent: .sourceCodeFormatting(),
                    permissions: [
                        .writeToPackageDirectory(reason: "This command reformats source files")
                    ]
                ),
                dependencies: [
                    .product(name: "swift-format", package: "swift-format"),
                ]),
    ]
)

Two new plugin instructions had been accessible after I executed the plugin record command.

swift bundle plugin --list

# ‘format-source-code’ (plugin ‘MyCommandPlugin’ in bundle ‘Instance’)
# ‘generate-documentation’ (plugin ‘Swift-DocC’ in bundle ‘SwiftDocCPlugin’)
# ‘preview-documentation’ (plugin ‘Swift-DocC Preview’ in bundle ‘SwiftDocCPlugin’)

Tried to run the primary one, and happily the doccarchive file was generated. 😊

swift bundle generate-documentation
# Producing documentation for 'Instance'...
# Construct full! (0.16s)
# Changing documentation...
# Conversion full! (0.33s)
# Generated DocC archive at '/Customers/tib/Linter/.construct/plugins/Swift-DocC/outputs/Instance.doccarchive'

Additionally tried to preview the documentation, there was a be aware concerning the –disable-sandbox flag within the output, so I merely added it to my authentic command and…

swift bundle preview-documentation 
# Word: The Swift-DocC Preview plugin requires passing the '--disable-sandbox' flag
swift bundle --disable-sandbox preview-documentation

Magic. It labored and my documentation was accessible. Now that is how plugins ought to work, I liked this expertise and I actually hope that an increasing number of official plugins are coming quickly. 😍

Constructing a customized intent command plugin

I needed to construct a small executable goal with some bundled sources and see if a plugin can deploy the executable binary with the sources. This might be very helpful after I deploy feather apps, I’ve a number of module bundles there and now I’ve to manually copy every little thing… 🙈


import PackageDescription

let bundle = Bundle(
    title: "Instance",
    platforms: [
        .macOS(.v10_15),
    ],
    merchandise: [
        .library(name: "Example", targets: ["Example"]),
        .executable(title: "MyExample", targets: ["MyExample"]),
        .plugin(title: "MyCommandPlugin", targets: ["MyCommandPlugin"]),
        .plugin(title: "MyDistCommandPlugin", targets: ["MyDistCommandPlugin"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-format", exact: "0.50600.1"),
        .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

    ],
    targets: [
        .executableTarget(name: "MyExample",
                          resources: [
                            .copy("Resources"),
                          ], plugins: [
                            
                          ]),
        .goal(title: "Instance", dependencies: []),
        .testTarget(title: "ExampleTests", dependencies: ["Example"]),
       
        .plugin(title: "MyCommandPlugin",
                functionality: .command(
                    intent: .sourceCodeFormatting(),
                    permissions: [
                        .writeToPackageDirectory(reason: "This command reformats source files")
                    ]
                ),
                dependencies: [
                    .product(name: "swift-format", package: "swift-format"),
                ]),
        
        .plugin(title: "MyDistCommandPlugin",
                functionality: .command(
                    intent: .customized(verb: "dist", description: "Create dist archive"),
                    permissions: [
                        .writeToPackageDirectory(reason: "This command deploys the executable")
                    ]
                ),
                dependencies: [
                ]),
    ]
)

As a primary step I created a brand new executable goal known as MyExample and a brand new MyDistCommandPlugin with a customized verb. Contained in the Sources/MyExample/Assets folder I’ve positioned a easy take a look at.json file with the next contents.

{
    "success": true
}

The principle.swift file of the MyExample goal appears like this. It simply validates that the useful resource file is offered and it merely decodes the contents of it and prints every little thing to the usual output. 👍

import Basis

guard let jsonFile = Bundle.module.url(forResource: "Assets/take a look at", withExtension: "json") else {
    fatalError("Bundle file not discovered")
}
let jsonData = strive Knowledge(contentsOf: jsonFile)

struct Json: Codable {
    let success: Bool
}

let json = strive JSONDecoder().decode(Json.self, from: jsonData)

print("Is success?", json.success)

Contained in the Plugins folder I’ve created a fundamental.swift file beneath the MyDistCommandPlugin folder.

import PackagePlugin
import Basis

@fundamental
struct MyDistCommandPlugin: CommandPlugin {
    
    func performCommand(context: PluginContext, arguments: [String]) throws {
        
        
    }
}

Now I used to be capable of re-run the swift bundle plugin –list command and the dist verb appeared within the record of obtainable instructions. Now the one query is: how can we get the artifacts out of the construct listing? Fortuitously the third instance of the instructions proposal is kind of comparable.

import PackagePlugin
import Basis

@fundamental
struct MyDistCommandPlugin: CommandPlugin {
    
    func performCommand(context: PluginContext, arguments: [String]) throws {
        let cpTool = strive context.software(named: "cp")
        let cpToolURL = URL(fileURLWithPath: cpTool.path.string)

        let outcome = strive packageManager.construct(.product("MyExample"), parameters: .init(configuration: .launch, logging: .concise))
        guard outcome.succeeded else {
            fatalError("could not construct product")
        }
        guard let executable = outcome.builtArtifacts.first(the place : { $0.sort == .executable }) else {
            fatalError("could not discover executable")
        }
        
        let course of = strive Course of.run(cpToolURL, arguments: [
            executable.path.string,
            context.package.directory.string,
        ])
        course of.waitUntilExit()

        let exeUrl = URL(fileURLWithPath: executable.path.string).deletingLastPathComponent()
        let bundles = strive FileManager.default.contentsOfDirectory(atPath: exeUrl.path).filter { $0.hasSuffix(".bundle") }

        for bundle in bundles {
            let course of = strive Course of.run(cpToolURL, arguments: ["-R",
                                                                    exeUrl.appendingPathComponent(bundle).path,
                                                                    context.package.directory.string,
                                                                ])
            course of.waitUntilExit()
        }
    }
}

So the one downside was that I used to be not capable of get again the bundled sources, so I had to make use of the URL of the executable file, drop the final path part and browse the contents of that listing utilizing the FileManager to get again the .bundle packages inside that folder.

Sadly the builtArtifacts property solely returns the executables and libraries. I actually hope that we’ll get assist for bundles as nicely sooner or later so this hacky resolution may be averted for good. Anyway it really works simply high quality, however nonetheless it is a hack, so use it rigorously. ⚠️

swift bundle --allow-writing-to-package-directory dist
./MyExample 
#Is success? true

I used to be capable of run my customized dist command with out additional points, after all you should use extra arguments to customise your plugin or add extra flexibility, the examples within the proposal are just about okay, however it’s fairly unlucky that there isn’t any official documentation for Swift bundle supervisor plugins simply but. 😕

Conclusion

Studying about command plugins was enjoyable, however at first it was annoying as a result of I anticipated a bit higher developer expertise relating to the software invocation APIs. In abstract I can say that that is only the start. It is identical to the async / await and actors addition to the Swift language. The characteristic itself is there, it is principally able to go, however not many builders are utilizing it every day. This stuff would require time and hopefully we’ll see much more plugins in a while… 💪

[ad_2]

LEAVE A REPLY

Please enter your comment!
Please enter your name here