Random errors for SwiftPM plugin

I'm trying to write a simple plugin. The same code sometimes errors out and sometimes works:

@main
struct InjectablePlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext,
                             target: Target) async throws -> [Command]
    {
        let tool = try context.tool(named: "InjectableTool")

        guard let enumerator = FileManager
            .default
            .enumerator(at: URL(fileURLWithPath: target.directory.string),
                        includingPropertiesForKeys: [.isRegularFileKey],
                        options: [.skipsHiddenFiles, .skipsPackageDescendants]),
            let urls = enumerator.allObjects as? [URL]
        else {
            throw InjectablePluginError.failedToListPackageDirectory
        }

        guard urls.count > 0 else {
            throw InjectablePluginError.noFilesToProcess
        }

        let commands: [Command] = urls.map {
            let filename = $0.lastPathComponent

            return .buildCommand(displayName: "Processing \(filename)",
                                 executable: tool.path,
                                 arguments: [],
                                 inputFiles: [Path($0.path)])
        }

        return commands
    }
}

This sometimes works, sometimes errors with:

Unexpected duplicate tasks: WriteAuxiliaryFile /Path/To/Script.sh

and sometimes with:

Cycle inside InjectableTests; building could produce unreliable results. This usually can be resolved by moving the shell script phase 'Processing InjectableTests.swift' so that it runs before the build phase that depends on its outputs.
Cycle details:
→ Target 'InjectableTests' has link command with output /Path/To/InjectableTests.swift
○ Target 'InjectableTests' has compile command for Swift source files
○ That command depends on command in Target 'InjectableTests': script phase “Processing InjectableTests.swift”

That error might be detailed for someone who understands how the dependency graph works in SPM, but for me, it just seems to make no sense since my plugin doesn't output anything (yet).

cc @abertelrud

The Xcode errors were bogus. It turns out:

  1. When there are no outputFiles listed for the command, swift build (in my case swift test, but irrelevant) silently fails (does not call the executable tool, but doesn't show any error either), while Xcode shows the totally unrelated errors above.

  2. When output files are listed, but the executable tool doesn't write anything at those paths both Xcode and swift build correctly complain (not very clearly, but they do say file not found - presumingly as the compiler tries to access the file)

I'm not sure if this behaviour is documented anywhere, I must have missed it if so, regardless I do think SPM should report an error if the outputFiles are empty and in order to execute the command they are required not to be.

My guess is that lower down the absence of outputs means the build system never has any indication the tool is necessary, since it essentially works backward from the final product to find out what it needs to do. It is the output being used by a further step that creates this vital link.

I am curious what your use case is. If the purpose is not to produce something, then it is not very clear what would be expected from an incremental build (which is probably why it seems unpredictable).

I was just starting to play around with plugins for the first time and wanted to see how everything works and this caught me off guard. Ultimately, I'll have output. Although I'd imagine tools that only analyze the code and send back diagnostics might not necessary have output?