How do I capture output of a SwiftLint BuildToolPlugin and save it to a file?

I want to save a json report of my SwiftLint BuildToolPlugin so I can use it elsewhere.

I am currently trying the following:

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",
					"--reporter", "json",
					">", "swiftlint.result.json"
				],
				environment: [:]
			)
		]
	}
}

But that basically boils down to the following command in which the quotes around the '>' character prevents creating an output file.

swiftlint lint \
	--in-process-sourcekit \
	--path BusinessModel \
	--config .swiftlint.yml \
	--reporter json ">" swiftlint.result.json

How do I get the output in a file?

I think you could make another executable target, and have the tool plugin call that, passing in everything you need (including the path to swiftlint?)

Your executable target can use Process to invoke swiftlint, and can capture the output and do what it wants with it.

Not sure what you mean with "Your executable target can use Process to invoke swiftlint".
SwiftLint is already a binary target in my spm lib. What is this Process thing you're referring to?

	.binaryTarget(
		name: "SwiftLintBinary",
		url: "https://github.com/juozasvalancius/SwiftLint/releases/download/spm-accommodation/SwiftLintBinary-macos.artifactbundle.zip",
		checksum: "cdc36c26225fba80efc3ac2e67c2e3c3f54937145869ea5dbcaa234e57fc3724"
	),
	.plugin(
		name: "SwiftLintPlugin",
		capability: .buildTool(),
		dependencies: ["SwiftLintBinary"]
	),

I mean that you need to create another target and write a small executable.

The plugin will invoke your executable, instead of invoking SwiftLintBinary.

Your executable can run SwiftLintBinary, and capture the output. It can then do whatever it wants with that output, including saving it to a json file.

Process is the Foundation class you use to run external processes and capture their input. Here's a basic example taken from the WWDC 2022 session:

        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
        process.arguments = ["log", "--pretty=format:- %an <%ae>%n"]

        let outputPipe = Pipe()
        process.standardOutput = outputPipe
        try process.run()
        process.waitUntilExit()

        let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: outputData, as: UTF8.self)

Aaaah, thanks. That sounds exactly what I need.

Did you get this working?

Yes I did.

I needed to capture the output like samdeane mentioned (see the code).

I also asked the SwiftLint guys for an option to write reports to a file. They implemented that, so in a future release we can just specify an output file instead of having to capture the console log.

private func swiftlint(packageDir: String, executablePath: Path) async throws {
		let process = Process()
		let outputPipe = Pipe()
		
		process.standardOutput = outputPipe
		process.executableURL = URL(fileURLWithPath: executablePath.string)
		process.arguments = [
			"lint",
			"--in-process-sourcekit",
			"--config", "\(packageDir)/.swiftlint.yml",
			"--reporter", "json"
		]
		
		try process.run()
		process.waitUntilExit()
		
		if process.terminationReason == .exit && process.terminationStatus == 0 {
			print("Linting was successful.")
		} else {
			let problem = "\(process.terminationReason):\(process.terminationStatus)"
			Diagnostics.error("Linting failed because: \(problem)")
			throw SwiftLintCommandPluginErrors.swiftLintPluginError
		}
		
		do {
			let path = "build/reports"
			print("Creating directory structure '\(path)'.")
			
			try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
			
			let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
			let output = String(decoding: outputData, as: UTF8.self)
			print("Creating swiftlint report at '\(packageDir)/build/reports/swiftlint.result.json'")
			
			try output.write(
				toFile: "\(packageDir)/build/reports/swiftlint.result.json",
				atomically: true,
				encoding: .utf8
			)
		} catch {
			throw error
		}
	}