SwiftLint icon indicating copy to clipboard operation
SwiftLint copied to clipboard

[Suggestion] - Swift Package Manager build tool support

Open BrentMifsud opened this issue 2 years ago • 23 comments

There isnt a suggestion ticket option so i just used a bug ticket template.

New Issue Checklist

Describe the bug

Apple just announced this is coming in the next beta of xcode. It would be amazing if this was supported.

from the recent patch notes:

# Swift Package Manager
## New Features
Swift Packages now support build tool plugins, as defined in [SE-0303](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) and [SE-0325](https://github.com/apple/swift-evolution/blob/main/proposals/0325-swiftpm-additional-plugin-apis.md). This allows packages to define plugins that can specify tools that should run during a build operation, for example to generate source code. This is supported in both swift package and in Xcode’s support for packages. (79876749)

The swift package command now supports command plugins, as defined in [SE-0332](https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md). This allows Packages to define commands that can be invoked using the swift package command line to perform custom actions on the package. (82895553)
Complete output when running SwiftLint, including the stack trace and command used

n/a

Environment

n/a

BrentMifsud avatar Feb 04 '22 16:02 BrentMifsud

It doesn’t seem like the first version of SwiftPM build tools will support linters or formatters: https://twitter.com/tonyarnold/status/1498508218934644737

jpsim avatar Mar 02 '22 15:03 jpsim

You can still use SwiftLint within a command plugin (which you call via something like swift package lint-my-project) - it's the integration with build and prebuild steps automatically for individual package targets that doesn't work right now.

I've put together a basic example to help diagnose bugs, but you're welcome to use it as a base for something more official: https://github.com/tonyarnold/swiftpm-buildtool-plugin-examples/tree/swiftlint-example/Example%205%20-%20SwiftLint

The limitation seems to be that there's no way for these steps to report information to Xcode (although this could be the intersection of a few different bugs SR-15929 and FB9937439).

tonyarnold avatar Mar 02 '22 22:03 tonyarnold

I was able to get swiftlint report warnings and errors to Xcode when using a .buildCommand. See the example project: https://github.com/juozasvalancius/ExampleSPMProjectWithSwiftLint

For me it only worked when the plugin is declared and used in the same Swift package. I tried to separate the plugin implementation in one package, and usage in another, but then Xcode wouldn't display any warnings, even the ones generated by the swift compiler.

Anyway, I opened a PR (https://github.com/realm/SwiftLint/pull/3912) that is required for that example project to work.

juozasvalancius avatar Mar 20 '22 15:03 juozasvalancius

I followed your example project and tested it on a library distributed as a Swift Package. It worked great! Thank you very much @juozasvalancius

jcabreram avatar Mar 22 '22 21:03 jcabreram

@juozasvalancius I tried your example and I made it work. I built yesterdays most recent SwiftLint binary locally (which included your changes) and added the binary as a local target, checking it in with the repository. I noticed that SPM sometimes had issues pulling the binary. I also managed to move the plugin in its own shared Package that other Package rely on. It did work as well without any issues, at least locally. It basically looks like this for me:

let package = Package(
    name: "PackagePlugins",
    platforms: [
        .iOS(.v13),
        .watchOS(.v7),
        .macOS(.v12)
    ],
    products: [
        .plugin(
            name: "Linting",
            targets: ["Linting"]
        )
    ],
    dependencies: [

    ],
    targets: [
        .binaryTarget(
            name: "SwiftLint",
            path: "Binaries/SwiftLint.artifactbundle" // <-- latest binary built from master
        ),
        .plugin(
            name: "Linting",
            capability: .buildTool(),
            dependencies: [
                "SwiftLint"
            ]
        )
    ]
)

The Plugin:

import PackagePlugin

@main
struct SwiftLintPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        [
            .buildCommand(
                displayName: "Running SwiftLint for \(target.name)",
                executable: try context.tool(named: "swiftlint").path,
                arguments: [
                    "lint",
                    "--in-process-sourcekit",
                    "--path",
                    target.directory.string,
                    "--config",
                    "\(context.package.directory.string)/etc/swiftlint.yml" //<-- only difference to your plugin
                ],
                environment: [:]
            )
        ]
    }
}

In the other Packages I add the dependency with:

.package(path: "../packageplugins")

and plugin to the target with:

plugins: [ .plugin(name: "Linting", package: "PackagePlugins") ]

I have yet to try how if this also works with non local plugins and the binaries pulled from github.

marcoboerner avatar Apr 01 '22 09:04 marcoboerner

Yep, I was also able to get this working in separate packages with the swiftlint binaries committed to git. Unfortunately, Xcode 13.3 appears buggy when specifying a URL for artifactbundle.

After the next swiftlint version is released, it might be useful to setup a repository that hosts only the release binaries (committed to git for now) and plugin implementations.

juozasvalancius avatar Apr 09 '22 23:04 juozasvalancius

Yep, I was also able to get this working in separate packages with the swiftlint binaries committed to git. Unfortunately, Xcode 13.3 appears buggy when specifying a URL for artifactbundle.

After the next swiftlint version is released, it might be useful to setup a repository that hosts only the release binaries (committed to git for now) and plugin implementations.

I was thinking this as well.

For linting use cases, there really isn't any reason to have the source code downloaded. You only really need the binary.

BrentMifsud avatar Apr 11 '22 20:04 BrentMifsud

I noticed that SPM sometimes had issues pulling the binary.

Have you filed a bug? I'm happy to make sure Apple folks working on SwiftPM take a look.

After the next swiftlint version is released, it might be useful to setup a repository that hosts only the release binaries (committed to git for now) and plugin implementations.

I'd really rather not do this. The universal macOS binary is currently 58MB so every release would add another 58MB to the repo size.

jpsim avatar Apr 11 '22 20:04 jpsim

With reference to this issue, I created a separate package to try out. It seems to work for me. https://github.com/usami-k/SwiftLintPlugin

But, I'm not sure that fixed issues pulling the binary...

usami-k avatar Apr 14 '22 23:04 usami-k

With reference to this issue, I created a separate package to try out. It seems to work for me. https://github.com/usami-k/SwiftLintPlugin

But, I'm not sure that fixed issues pulling the binary...

Hello @usami-k! I've tried your solution, but it didn't show any errors in Xcode inline. Errors were mentioned in build log, which is not really convenient. Do you have those errors as shown here? image

RomaJZ avatar Apr 22 '22 12:04 RomaJZ

I noticed that SPM sometimes had issues pulling the binary.

Have you filed a bug? I'm happy to make sure Apple folks working on SwiftPM take a look.

After the next swiftlint version is released, it might be useful to setup a repository that hosts only the release binaries (committed to git for now) and plugin implementations.

I'd really rather not do this. The universal macOS binary is currently 58MB so every release would add another 58MB to the repo size.

Github releases does that and that's what @usami-k is doing.

I wonder though, this is nice for your own packages but for your app's code I believe still using a build script in Xcode is necessary, is that right ?

quentinfasquel avatar Apr 24 '22 13:04 quentinfasquel

@usami-k I am not sure that SwiftLint is actually finding/using the .swiftlint.yml file when ran from the plugin, is it?

@RomaJZ I believe that Xcode 13.3.1 (13E500a) isn't reliable about showing the warning/errors in source, inline as you said. It worked for me on many occasion but it's not consistent.

Regardless, I found that updating your app to have a feature package is rather annoying and I figured that I could use swift package plugin swiftlint, this is possible with plugin with capability command . I made my own version of the SwiftLint plugin for testing purposes https://github.com/quentinfasquel/SwiftLint.Plugin , it has both a build tool plugin that can be used directly on a swift package target, and a command plugin that can be used on an Xcode project target, in a build script phase, see example below.

if [ $ACTION != "indexbuild" ]; then

SDKROOT=$(/usr/bin/xcrun --sdk macosx --show-sdk-path)
SWIFT_PACKAGES="${BUILD_DIR}/../../SourcePackages/checkouts"

swift package --package-path="${SWIFT_PACKAGES}/SwiftLint.Plugin" plugin swiftlint --path $PROJECT_DIR

fi

An another subject, I hope most swift package plugins that relate to an official tool will eventually be shipped through the tool's official repository.

If interested in R.swift, I made a swift plugin for it here https://github.com/quentinfasquel/R.swift.Plugin

quentinfasquel avatar Apr 24 '22 16:04 quentinfasquel

@quentinfasquel Actually it works, but not the way we all would prefer. I copied the whole plugin source code into my SPM and then it worked. So basically I create my own plugin target from source code in the same SPM that depends on SwiftLint binary that is loaded from GitHub. And only after that everything works and highlights all the errors inline. But that's not convenient so I'm looking forward to Apple to fix that issue with those plugins

RomaJZ avatar Apr 25 '22 20:04 RomaJZ

@RomaJZ Ok. I agree that it works, what I am saying is that it actually works in the way we all prefer, meaning when using either my url or @usami-k 's (remote plugin)

The issue in my opinion still exists and is on Xcode's side not being reliable when using both, in the same project :

  • SwiftLint as package plugin on a local package
  • SwiftLint used as package command plugin in project target script phase

quentinfasquel avatar Apr 25 '22 21:04 quentinfasquel

I've been experimenting with wrapping SwiftLint as a build tool plugin with Xcode 14.0 beta and it runs fine, with a couple of caveats:

  1. SwiftLint's output isn't showing up for me in the editor windows. I can see the diagnostics in the build log. I filed a FB with Apple about this. (Edit: this is only a problem with .prebuildCommand; .buildCommand works.)
  2. If linting find an error, it exits with a status code of 2, which Xcode interprets as a fatal build failure. While I could wrap the SwiftLint invocation in a shell script to hide the exit status, I'd find it cleaner if a new option could be added to SwiftLint to always return with an exit status of 0. (Edit: on further thought, maybe failing the build on error is desirable.)

sjmadsen avatar Jun 14 '22 15:06 sjmadsen

Do we know if an official version of the swiftlint plugin is on the horizon? It seems like the plugins posted above work well but I'd prefer to use an official distribution, if possible.

NiallBegley avatar Jun 23 '22 18:06 NiallBegley

Anyone have anecdotal thoughts on whether making this a Swift plugin has any impact (positive or negative) on build times vs just using a shell-script build phase?

jaredgrubb avatar Jun 24 '22 16:06 jaredgrubb

SwiftLint's output isn't showing up for me in the editor windows. I can see the diagnostics in the build log. I filed a FB with Apple about this. (Edit: this is only a problem with .prebuildCommand; .buildCommand works.)

@sjmadsen Is this your experience? I'm seeing unpredictable behavior with both .buildCommand and .prebuildCommand. Seems like sometimes it decides to work and other times it won't. Has anyone figured out this aspect of it yet?

NiallBegley avatar Jun 24 '22 20:06 NiallBegley

I haven't tried prebuildCommand again lately. Xcode sometimes stops running the plugin, though. A restart seems to fix it.

sjmadsen avatar Jun 24 '22 21:06 sjmadsen

@quentinfasquel I used your solution and it works great for me! With a build tool I can run the linter automatically on builds but I also can execute the command as part of my CI, which is perfect.

The only problem i'm having is if I have another package incorporate the package that has the SwiftLint plugin, I get the following error:

product 'SwiftLint.Plugin' required by package '[Package A]' target '[Package A Target]' not found in package 'SwiftLint.Plugin'.

Visually, this is what's going on

         Swift Package B
                   |
                   |
                   |
                   |
    Swift Package A (dependency)
                    |
                    |
                    |
           SwiftLint Plugin

Does anyone know what I might be doing wrong? Package B's package file looks like this:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Package B",
    platforms: [
        .iOS(.v13),
        .macOS(.v10_15),
    ],
    products: [
        .library(
            name: "Package B",
            targets: ["Package B"]),
    ],
    dependencies: [
        
        .package(
            name: "Package A",
            path: "../package-a"),
    ],
    targets: [
        .target(
            name: "Package B",
            dependencies: [
                .product(name: "Package A", package: "Package A"),
            ]
    ]
)

edit: I believe i've solved this, although I'm a little worried about the implications of the changes I've made (which i don't fully understand yet, i'm new to SPM). I moved the SwiftLint.Plugin out of being a plugin on the target and instead added it as a straight up dependency to the target instead in both Package A and B.

Additionally, using Xcode 14.0 beta 2 I've discovered that the lint errors/warnings not showing up on the Warnings/Errors tab seems to have been fixed. Or maybe that was the result of me removing SwiftLing.plugin as a plugin? I'm not 100% sure.

NiallBegley avatar Jun 29 '22 19:06 NiallBegley

edit: I believe i've solved this, although I'm a little worried about the implications of the changes I've made (which i don't fully understand yet, i'm new to SPM). I moved the SwiftLint.Plugin out of being a plugin on the target and instead added it as a straight up dependency to the target instead in both Package A and B.

@NiallBegley what does your resulting package.swift end up looking like for both "Package A" and "Package B"?

daSkier avatar Jul 15 '22 14:07 daSkier

Hello ! As Xcode 14 is now released, any news on supporting SPM plugins ?

clementnonn avatar Sep 15 '22 14:09 clementnonn

There is currently work going on in #4176.

SimplyDanny avatar Sep 15 '22 15:09 SimplyDanny

Done in 3fd1573c572f5d960d8e8b3bc46bc229c969ddcb

jpsim avatar Oct 05 '22 19:10 jpsim

@jpsim any reason the plugin isn't using a .binaryTarget referencing the artifactbundle provided with every release?

The way the plugin is implemented now it takes a lot of time to compile which seems unnecessary.

weakfl avatar Nov 03 '22 08:11 weakfl

@weakfl sorry I missed your comment. Next time please open a new issue for better visibility instead of commenting on closed tickets.

Last I looked at it, plugins using binary targets was only supported on macOS. Furthermore, using binary artifacts from GitHub Releases would mean that pointing the plugin to non-release commits of SwiftLint wouldn't work since we don't publish binary assets for every commit.

That being said, the current source-based plugin implementation is causing other issues (e.g. https://github.com/realm/SwiftLint/issues/4558) and I'm considering offering both a binary-based plugin and a source-based plugin.

jpsim avatar Nov 25 '22 16:11 jpsim