Preventing SPM package's dependencies from being exported when importing the package?

Is there a way to prevent a Swift Package Manager package from exposing the methods and objects of its own dependencies to the applications that use the package?

Here's current observed behavior:

  • PackageA, a Swift static library (no framework, just source files)
    • This contains an SPM dependency declared in its Package.swift manifest:
      • PackageB, a static library (no framework, just source files)
  • In Xcode, in a Application .xcproject , PackageA gets added as a dependency through the Swift Package Manager via its GitHub repo URL
    • *.swift source files do not see any of PackageA or PackageB contents until they are imported (which is normal)
    • As soon as you import PackageA in a *.swift file, both PackageA and PackageB are exposed in that scope (this is undesirable)

My expectation would be that only PackageA's symbols be exposed to the Application by default and not also expose all of its dependencies' symbols. I would expect the methods in PackageB would only be exposed if the Application also imports PackageB explicitly in a *.swift file's scope.

Is there a way to prevent PackageB from being imported when only import PackageA is used?

(What makes this additionally confounding is that if you open the PackageA package itself in Xcode, PackageB is only available when import PackageB is denoted, which makes sense. However, when an application imports PackageA, then unconditionally PackageB is exposed to the application every time. This seems incorrect behavior.)

tl;dr

Basically, I have a common shared utilities code module (SPM PackageB) that all of my open source libraries (ie: SPM PackageA) depend on. But I don't want consumers of these libraries to know or care about the shared code module - its methods and objects should not be getting imported into the consumer's projects, they are meant to be internal to the consumable package. And because the shared code has heavy use of extensions on common types, it increases the risk of namespace collisions and category pollution for consumers. I would prefer to keep the shared code as a SPM package and not a git submodule.

Right now you can do this by using @_implementationOnly for the imports (e.g. @_implementationOnly import MyUtilityPackage. Note that this is a bit of a hefty hammer: it is mostly implemented with an eye towards resilient libraries, and so you may find it isn’t actually possible for you to use it (e.g. you cannot have stored properties that use any type from MyUtilityPackage).

Sadly, that’s the required way to do this.

4 Likes

Thank you. I spent hours googling and trying to figure this out before heading here.

So far in a number of scenarios I've tested, your solution has not presented any issues and is working as desired.

Side note: this behavior isn't unique to packages, you would also need to use implementation-only imports if you use any other way of sharing your library.

1 Like