I'm currently writing an SPM package to house several utility plugins for a codebase, and these plugins overlap in functionality. Because of this, they currently duplicate code verbatim that I'd love to factor out — but I've run into a few difficulties in doing so.
My initial approach was to factor out the common code into a shared "Common" target that could be exposed to these plugins:
// Package.swift
import PackageDescription
let package = Package(
name: "…",
products: [
.plugin(name: "Plug1", targets: ["Plug1"]),
.plugin(name: "Plug2", targets: ["Plug2"]),
],
targets: [
.target(name: "Common"),
.plugin(name: "Plug1", capability: .buildTool(), dependencies: ["Common"]),
.plugin(name: "Plug2", capability: .command(intent: ...), dependencies: ["Common"]),
]
)
The file hierarchy:
.
├── Package.swift
├── Plugins
│ ├── Plug1
│ │ └── plugin.swift
│ └── Plug2
│ └── plugin.swift
├── README.md
└── Sources
└── Common
└── Common.swift
Unfortunately, with this setup, it appears that neither plugin is able to see or import a Common module, nor use any of its contents: attempting to do so just results in No such module 'Common'.
- The
Common target is being built: if I #error in one of its source files, the build fails as expected
- All of the contents of
Common are marked as public
- If I add another target, it's able to see, import, and use the
Common module even if I don't add Common as a dependency; the package builds just fine if neither plugin attempts to use the shared code
- Adding
Common as a public product of the package yields no change
- Moving the module sources on disk (adjusting
path and sources for the target) doesn't appear to change visibility
So, am I doing something wrong here? Is this scenario expected to work, or is this being disallowed for reasons I'm unaware of?
As an alternative approach, I tried sharing code more directly by directly listing target sources and including Common.swift in both Plug1 and Plug2, as well as attempting to merge Plug1 and Plug2 by making one type which offers both BuildToolPlugin and CommandPlugin interfaces, but both of these attempts failed as SPM doesn't allow overlapping sources to be used in multiple targets.
While I understand this restriction for regular source libraries (where multiple targets can be linked together to lead to linking conflicts), should this apply for plugin targets? My gut feeling says "no" — as far as I can tell, plugins can't reasonably be linked together in a way that makes overlapping sources a concern. But if I'm wrong on this, I'd love to learn something new!
Happy to file issues / Feedback, but wanted to make sure first this wasn't user error, and that these scenarios are ones we'd actually like to support.
Tangential Follow-Up Question
Some of the code I was hoping to share in Common actually contains extensions to types vended by PackagePlugin and XcodeProjectPlugin — but it appears that those modules are only exposed to .plugin targets, and are inaccessible anywhere else (No such module 'PackagePlugin'). Is this also intentional? (I can sort of see this making sense as those modules don't make much sense for use outside of .plugin targets, but this also feels like a limitation that would be useful to lift without being risky.)