Swift packages and module dependencies

Hi there,

One of the great things about the Swift package system is the ability to separate things into modules and use extensions to add additional functionality. For instance, a base class (e.g. data schema class) shared between server and iOS software can be augmented for each case in separate server-specific and iOS-specific modules via extensions and conformances. Brilliant! Also dependencies can be explicitly stated in Package.swift ensuring that the module architecture is both explicit and enforced (stray import statements can be caught....). And modules can be turned into Packages with their own repos as things evolve.

I recently tried to really test this by re-factoring a reasonably large Swift package into multiple modules with carefully designed architecture (dependency) rules and to allow for future migration of some modules into packages. There were a fair number of challenges most of which are uninteresting to report (e.g. need to frequently clean build folder before re-build to catch Package.swift changes). But I want to report two things here which I am hoping might be fixable or able to be improved.

First, it seems there is a loophole in enforcement of Package.swift dependency rules. Let's say we create a Package.swift where module A depends on module B but not module C and module B depends on module C. Then of course in module A we can import from module B without error. But in Xcode 11.2 beta we can also import from module C into module A even though a direct dependency is not stated in Package.swift. And in fact it seems perhaps imports from all indirect dependencies are allowed by the compiler. If there is no way to block imports of indirect dependencies this is a real problem. For example, in the case described, module B may be an abstraction layer for low-level module C which should not be exposed to customer module A. But there is no way to restrict imports from module C in module A source code. Now maybe there is a way I don't know of but shouldn't all module dependencies in fact be explicit (and removable) in the Package.swift file?

The second issue relates to automatic "re-export" of extensions and conformances added to imported public types. As a trivial example, if in two different modules we have the common conformance extension String: Error {}, we get a redundant conformance error when importing the first into the second because the conformance is "re-exported". (And, as an aside, if we have something like extension String { public var myVar: String { return "myText" } } in both with different myText, according to my testing we actually get silent overriding of myVar in the importing module rather than a re-declaration error!!) Unwanted extension and conformance leakage between modules can cause issues like name clashes as well as redundant conformances. The consequence of these issues is that modules need to be designed for a specific package rather than as generic re-usable blocks which can be re-used between packages and evolved into their own package. This is unfortunate. For instance, to handle extension String: Error {} we can put this in a base module so it is defined once in a package. But now other modules in the package need to depend on this base module and cannot be re-used as simply elsewhere.

It would be really nice if there were some way to control the "re-export" of changes to imported public types. Could we e.g. use a syntax something like import X as internal to stop conformances and extensions added to public type X being "re-exported"? This would aid designing modules as re-usable units with clear functional dependencies.

BR,
Mark

2 Likes

You'll probably be interested in the work being done on implementation-only imports, which you can start playing with in Swift 5.1 (albeit behind a "private" attribute, @_implementationOnly, so it's not 100% ready yet).

1 Like

Thanks for the pointer, seem like these issues are known and being addressed which is great. I've added a quick comment on that thread.