Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Package and `#if canImport(...)`. How does it work?

Excuse the vague title.

I'm trying to build a package to help me use third-party cloud storage APIs (Firebase Storage for example), adding Combine support, etc. This package does the same thing with CloudKit. Everything compiles fine, but when I import the package module into a separate project of mine, the module is apparently missing some public symbols...

Specifically, the ones wrapped inside of an #if canImport(FirebaseStorage) condition. Since Firebase doesn't support SwiftPM yet, this part of the package behaves as expected in the package project itself; it simply skips compiling that whole bit. I figured that a client project that can import this module would compile it fine.

Aside: What I'm trying to do looks something like optional dependencies. I don't want to have to import Firebase to use this package's other features. I have considered splitting the package into separate sub-packages, each depending upon the particular third-party library I want to use. I might do that anyway. But the problem remains that Firebase doesn't yet support SwiftPM (although I hear they're close).

My issue appears similar to this one. My client project just doesn't seem to see the conditioned symbols, though it can import Firebase and FirebaseStorage just fine! I mean that the generated module header is missing them entirely, preventing my client project from compiling when I use them.

It seems to me that the compile condition never leaves the package's own scope of dependent targets. Is this the case? Or am I missing something obvious? I had always assumed that Swift Packages just import and compile the Swift source files into named modules, but now I think that is not so.

Is there a way to build code into a Swift Package that compiles only when the client can import a third-party module that does not yet support SwiftPM? Or does conditional compilation not work that way?

EDIT: Here is the Swift documentation on conditional compilation, for reference.

like image 985
AverageHelper Avatar asked Sep 07 '25 21:09

AverageHelper


2 Answers

(Answer from experience in Apr 2020)

It looks like I'm just misunderstanding the compile order.

Importing my packaged module (let's call it CloudStorage) declares a dependency in the client project to that module. Before the client project can compile with its other dependencies, CloudStorage needs to compile without the main project's dependencies. Since CloudStorage doesn't know anything about those dependencies, canImport for those dependencies evaluates to false.

This may have changed in a later version of Swift. I've yet to try again.

like image 176
AverageHelper Avatar answered Sep 10 '25 13:09

AverageHelper


canImport checks whether the module name provided can be imported. For swift packages, this evaluates to true if you have provided the module as a target dependency in the package manifest and all of its associated target dependency condition are satisfied.

The benefit of this is this allows you to write code in a platform agnostic way and let your package manifest take care of platform support.

Let's say I have Firebase as a dependency in my target:

.product(name: "Firebase", package: "firebase-ios-sdk", condition: .when(platforms: [.iOS, .macOS, .tvOS])),

I can write my code that depends on Firebase with canImport. Suppose in future firebase-sdk started supporting another platform, I can add the platform to my availability condition and start supporting that platform in my code as well.

But if you don't have the module listed as dependency, even if your client app can import the module, this condition will always evaluate to false. This is due to the sandbox nature of swift package build system and all your package targets are isolated so that consuming client's build settings doesn't affect your package target.

like image 25
Soumya Mahunt Avatar answered Sep 10 '25 12:09

Soumya Mahunt