Files
SwiftLint/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift
2025-06-25 19:39:45 +00:00

146 lines
6.2 KiB
Swift

import Foundation
import PackagePlugin
@main
struct SwiftLintBuildToolPlugin: BuildToolPlugin {
func createBuildCommands(
context: PluginContext,
target: Target
) throws -> [Command] {
try makeCommand(executable: context.tool(named: "swiftlint"),
swiftFiles: (target as? SourceModuleTarget).flatMap(swiftFiles) ?? [],
environment: environment(context: context, target: target),
pluginWorkDirectory: context.pluginWorkDirectory)
}
/// Collects the paths of the Swift files to be linted.
private func swiftFiles(target: SourceModuleTarget) -> [Path] {
target
.sourceFiles(withSuffix: "swift")
.map(\.path)
}
/// Creates an environment dictionary containing a value for the `BUILD_WORKSPACE_DIRECTORY` key.
///
/// This method locates the topmost `.swiftlint.yml` config file within the package directory for this target
/// and sets the config file's containing directory as the `BUILD_WORKSPACE_DIRECTORY` value. The package
/// directory is used if a config file is not found.
///
/// The `BUILD_WORKSPACE_DIRECTORY` environment variable controls SwiftLint's working directory.
///
/// Reference: [https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7](
/// https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7
/// )
private func environment(
context: PluginContext,
target: Target
) throws -> [String: String] {
let workingDirectory: Path = try target.directory.resolveWorkingDirectory(in: context.package.directory)
return ["BUILD_WORKSPACE_DIRECTORY": "\(workingDirectory)"]
}
private func makeCommand(
executable: PluginContext.Tool,
swiftFiles: [Path],
environment: [String: String],
pluginWorkDirectory path: Path
) throws -> [Command] {
// Don't lint anything if there are no Swift source files in this target
guard !swiftFiles.isEmpty else {
return []
}
// Outputs the environment to the build log for reference.
#if DEBUG
print("Environment:", environment)
#endif
let arguments: [String] = [
"lint",
"--quiet",
// We always pass all of the Swift source files in the target to the tool,
// so we need to ensure that any exclusion rules in the configuration are
// respected.
"--force-exclude",
]
// Determine whether we need to enable cache or not (for Xcode Cloud we don't)
let cacheArguments: [String]
if ProcessInfo.processInfo.environment["CI_XCODE_CLOUD"] == "TRUE" {
cacheArguments = ["--no-cache"]
} else {
let cachePath: Path = path.appending("Cache")
try FileManager.default.createDirectory(atPath: cachePath.string, withIntermediateDirectories: true)
cacheArguments = ["--cache-path", "\(cachePath)"]
}
let outputPath: Path = path.appending("Output")
try FileManager.default.createDirectory(atPath: outputPath.string, withIntermediateDirectories: true)
return [
.prebuildCommand(
displayName: "SwiftLint",
executable: executable.path,
arguments: arguments + cacheArguments + swiftFiles.map(\.string),
environment: environment,
outputFilesDirectory: outputPath),
]
}
}
#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin
// swiftlint:disable:next no_grouping_extension
extension SwiftLintBuildToolPlugin: XcodeBuildToolPlugin {
func createBuildCommands(
context: XcodePluginContext,
target: XcodeTarget
) throws -> [Command] {
try makeCommand(executable: context.tool(named: "swiftlint"),
swiftFiles: swiftFiles(target: target),
environment: environment(context: context, target: target),
pluginWorkDirectory: context.pluginWorkDirectory)
}
/// Collects the paths of the Swift files to be linted.
private func swiftFiles(target: XcodeTarget) -> [Path] {
target
.inputFiles
.filter { $0.type == .source && $0.path.extension == "swift" }
.map(\.path)
}
/// Creates an environment dictionary containing a value for the `BUILD_WORKSPACE_DIRECTORY` key.
///
/// This method locates the topmost `.swiftlint.yml` config file within the project directory for this target's
/// Swift source files and sets the config file's containing directory as the `BUILD_WORKSPACE_DIRECTORY` value.
/// The project directory is used if a config file is not found.
///
/// The `BUILD_WORKSPACE_DIRECTORY` environment variable controls SwiftLint's working directory.
///
/// Reference: [https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7](
/// https://github.com/realm/SwiftLint/blob/0.50.3/Source/swiftlint/Commands/SwiftLint.swift#L7
/// )
private func environment(
context: XcodePluginContext,
target: XcodeTarget
) throws -> [String: String] {
let projectDirectory: Path = context.xcodeProject.directory
let swiftFiles: [Path] = swiftFiles(target: target)
let swiftFilesNotInProjectDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: projectDirectory) }
guard swiftFilesNotInProjectDirectory.isEmpty else {
throw SwiftLintBuildToolPluginError.swiftFilesNotInProjectDirectory(projectDirectory)
}
let directories: [Path] = try swiftFiles.map { try $0.resolveWorkingDirectory(in: projectDirectory) }
let workingDirectory: Path = directories.min { $0.depth < $1.depth } ?? projectDirectory
let swiftFilesNotInWorkingDirectory: [Path] = swiftFiles.filter { !$0.isDescendant(of: workingDirectory) }
guard swiftFilesNotInWorkingDirectory.isEmpty else {
throw SwiftLintBuildToolPluginError.swiftFilesNotInWorkingDirectory(workingDirectory)
}
return ["BUILD_WORKSPACE_DIRECTORY": "\(workingDirectory)"]
}
}
#endif