'How to integrate SwiftLint with an iOS app using Swift Package Manager?

I'm creating a new iOS app in Xcode 11 (beta 5) and I'd like to try using Swift Package Manager instead of CocoaPods for managing dependencies.

A common pattern when using SwiftLint and CocoaPods is to add SwiftLint as a dependency and then add a build phase to execute ${PODS_ROOT}/SwiftLint/swiftlint; this way all developers end up using the same version of SwiftLint.

If I try to add SwiftLint as a SwiftPM dependency in Xcode, the executable target that I need is disabled:

Add Package Screenshot

I was able to fake it by creating a dummy Package.swift with no products or targets and running swift run swiftlint in my build phase, but it feels hacky and weird:

// swift-tools-version:5.1
import PackageDescription

let package = Package(
    name: "dummy-package",
    products: [],
    dependencies: [
        .package(url: "https://github.com/realm/SwiftLint.git", from: "0.34.0")
    ],
    targets: []
)

Is there a way do this without creating a dummy package? Or is Swift Package Manager just not the right tool for this particular use case?



Solution 1:[1]

I use xcodegen to genreate a Xcode project that has the ability to run scripts. This lets me see swiftlint warnings in Xcode while developing packages.

This tool creates a Xcode project from a project.yml definition. In that definition, you can add a script that runs swiftlint as a post compile task. Example.

Advantages of this method:

  • swiftlint warnings in Xcode.
  • Xcode settings beyond what SPM offers.

Disadvantages:

  • You rely on a third-party tool that could break or go away. However, you can drop this dependency at any time and go back to edit the Package.swift in Xcode.
  • You need to learn to write project.yml files.
  • If you use SPM bundles you need to generate the bundle accessor yourself.

A word about generating a bundle accessor. This is needed when working from a Xcode project because only SPM generates the file resource_bundle_accessor.swift to the project. If you already compiled after opening the Package.swift with Xcode, the file should be here:

find ~/Library/Developer/Xcode/DerivedData* -iname resource_bundle_accessor.swift

You can add it to the project, but if you are creating a framework, the bundle accessor can be as simple as:

import class Foundation.Bundle

// This file is only used when using a xcodegen-generated project.
// Otherwise this file should not be in the path.

private class BundleFinder {}

extension Foundation.Bundle {
    static var module = Bundle(for: BundleFinder.self)
}

Solution 2:[2]

Not perfect solution, but it works. I've found it here

  1. Modify target section of Package.swift
targets: [
    // 1. Specify where to download the compiled swiftlint tool from.
    .binaryTarget(
        name: "SwiftLintBinary",
        url: "https://github.com/realm/SwiftLint/releases/download/0.47.1/SwiftLintBinary-macos.artifactbundle.zip",
        checksum: "cdc36c26225fba80efc3ac2e67c2e3c3f54937145869ea5dbcaa234e57fc3724"
    ),
    // 2. Define the SPM plugin.
    .plugin(
      name: "SwiftLintPlugin",
      capability: .buildTool(),
      dependencies: ["SwiftLintBinary"]
    ),
    // 3. Use the plugin in your project.
    .target(
        name: "##NameOfYourMainTarget",
        dependencies: ["SwiftLintPlugin"] // dependency on plugin
    )
  ]
  1. Create Plugins/SwiftLintPlugin/SwiftLintPlugin.swift
import PackagePlugin

@main
struct SwiftLintPlugin: BuildToolPlugin {
  func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
    return [

        .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)/.swiftlint.yml" // IMPORTANT! Path to your swiftlint config
            ],
            environment: [:]
        )
    ]
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Constantine Nikolsky