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 aspublic
- If I add another target, it's able to see, import, and use the
Common
module even if I don't addCommon
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
andsources
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.)