Xcode attempts to build plugins for iOS. Is there a workaround?

I know we don't usually talk Xcode here, but my hope is that someone with good SPM knowledge might help find a workaround. Tagged it off-topic just in case.

Right now (Xcode 13.3.1) attempts to build plugins for the target platform when plugins should only be built and run on the host platform (in my case macOS).

This means that something as basic as this won't work:

.executableTarget(
    name: "DemoTool",
    dependencies: [
        .product(name: "SwiftSyntax", package: "swift-syntax"),
        .product(name: "SwiftSyntaxParser", package: "swift-syntax"),
        .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
    ]
),
.plugin(
    name: "DemoPlugin",
    capability: .buildTool(),
    dependencies: [
        .target(name: "DemoTool"),
    ]
),

because SwiftSyntax has a binary dependency to lib_InternalSwiftSyntaxParser.dylib packed in a macOS-only xcframework. When Xcode tries to compile DemoTool for iOS/Sim you'll get something within the lines of:

While building for iOS Simulator, no library for this platform was found in / ../../_InternalSwiftSyntaxParser.xcframework

Now, I understand why Xcode wants to compile the executable target, but how can I prevent that? There seems to be no way of distinguishing between targets used by the plugin, always ran on the host platform and regular executable targets.

EDIT: I'm looking for a workaround that doesn't imply packing the tool as a binary dependency. That works, but sadly fails at the next step for other reasons (it seems Xcode doesn't fully support artifactbundle yet)

I do not know of a way to make it work in Xcode 13.3.1. The first development script I tried to convert into a plug‐in ran into this problem. What I ended up with was the following ugly workaround:

let package = Package(
  // ...
  targets: [
    // ...
    .target(
      // ...
      exclude: ["Generated"],
      plugins: ["DataGenerator"]
    ),
    // ...
    .plugin(
      name: "DataGenerator",
      capability: .buildTool(),
      dependencies: ["generate‐data"]
    ),
    .plugin(
      name: "DataGeneratorCommand",
      capability: .command(
        intent: .custom(verb: "generate‐data", description: "Generates data."),
        permissions: [.writeToPackageDirectory(reason: "Inserts generated source files.")]
      ),
      dependencies: ["generate‐data"]
    ),
    .executableTarget(
      name: "generate‐data",
      dependencies: [
        // ...
      ]
    ),
    // ...
  ]
)

import Foundation
for platform in ["TVOS", "IOS", "WATCHOS"] {
  if ProcessInfo.processInfo.environment["TARGETING_\(platform)"] == "true" {
    let impossible: [Target.TargetType] = [.executable, .plugin]
    package.targets.removeAll(where: { impossible.contains($0.type) })
    for target in package.targets {
      target.plugins = nil
      target.exclude = []
    }
  }
}

That way it was possible to build for iOS by first setting TARGETING_IOS in the terminal. But it still required manually running swift package plugin --allow-writing-to-package-directory generate‐data and checking it into source control (not so bad with a Git hook) It was basically equivalent to the status quo before I tried to make it a plug‐in.

I kept the build tool integrated for macOS/Linux for no other reason than keeping a record of how it had been done so as not to have to figure it out again later, and ensuring it stayed up to date in the meantime. It really is completely redundant at that point.

Needless to say, I never again attempted to add a build tool to any package supporting iOS, and would not recommend it until Xcode is fixed.

Thanks @SDGGiesbrecht for the sample code. In my case, it's a library that I plan to release to third parties and I think I'd rather distribute it via homebrew than shipping with those steps with it and hoping users won't mess them up. Hopefully Xcode will fix this in future versions.

You can just treat all platforms the same as a command plugin with its result checked into source control. (Basically just remove the if ProcessInfo... from above and do it unconditionally.) That way your clients can at least use it as a package.

In the end I've succumbed to binary dependencies. It seems Xcode works with local ones and only fails when they're fetched via URLs. This will do for now. Thanks for the help!