SPM build fails for watchOS libraries

I develop libraries for all Apple platforms (macOS, iOS, tvOS, watchOS). I notice it is impossible to build a library in Xcode when the library scheme is selected and it targets the Apple watch simulator.

You can reproduce the error very easily:

  1. swift package init --type library

  2. In the Package.swift file add any platform/version you support. For example:

    platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)]
    
  3. Open with Xcode, select an Apple watch simulator as the scheme target, and try to build.

  4. Error: XCTest is not available when building for watchOS simulator.

It is well know that Apple watch doesn't support XCTest. However, I am unable to skip the test target uniquely for apple watch builds. Using the conditional compilation #if !os(watchOS) doesn't solve the problem.

What is the best way to open source a library (containing test targets) which targets all Apple platforms?

Am I missing something? Can my customers include a library in their watchOS apps and try in the simulator without getting build fails?

2 Likes

This happens to Alamofire too. Thankfully, it doesn’t affect consumers of the framework, just direct builders of the package. Alamofire still uses Xcode projects so we don’t have to deal with this issue. I think you can generate an Xcode project and turn off the test building there, but that won’t fix direct SPM builds.

I see. Thanks for the confirmation @Jon_Shier and for the hint on how to patch it. I'd rather avoid committing a whole Xcode project just for that, though.

I became aware of the issue by checking the Swift Package Index platform compatibility table. I guess other multi-platform libraries which don't commit Xcode projects are getting marked as not supporting watchOS when they actually do.

Since this is configured in the scheme, you can do the same for standalone packages and commit the changed scheme as a shared scheme.

But that only takes affect for Xcode builds, right? Or does SPM always pick it up, even on Linux?

I got a reply on my radar for this issue saying that the behavior can’t be changed because generate-xcodeproj also works this way. Can we just change the fundamental behavior? It doesn’t seem to be what people would want or expect.

It doesn't change the SwiftPM behaviour which has always been to build tests during swift build. The behaviour in Xcode is in place to mirror that and so was generate-xcodeproj.

I don't think we would flat out change the behaviour here, this is definitely what I expect because I don't want to break tests while just building. I think it would make sense to offer the same flexibility as Xcode in commandline SwiftPM, though, so that people can configure it based on their preference.

I think when discussing such an enhancement, we could evaluate what the default should be and potentially it should be the reverse of what it is today.

I mean, it certainly doesn't make sense as a default for platforms which don't support XCTest, so I'd consider it a bug that SPM even attempts to build tests on watchOS. And while I'm certainly in favor of changing the default behavior here, would it be possible to make this behavior configurable first? Perhaps just adding an option (--no-tests) to swift build would be enough to fix this in the short term.

The default of "building tests when building a target" seems sensible to me. I would definitely want to know if some tests are breaking at build time. I would just like to define which platform a test target is for. That is, I would love to say test target A is for macOS and iOS, and test target B is for Linux (for example).

I agree on the watchOS issue, but how does a flag in swift build help there, since that doesn't even support watchOS (safe for the experimental --build-system xcode option and some configuration trickery).

I see this as two separate issues:

There's a potential third topic to address here which is the disparity between swift build options and schemes. I think there's a design space here where the package should be able to express opinions on these configuration choices and they would apply equally to swift build and Xcode. That configuration would also be available to other libSwiftPM clients so that no matter the tool you're using, these preferences (such as building tests or not) could be respected.

This is what I do:

Since CI builds are from the command line, I simply have that environment variable set. On the rare occasion something fails only on watchOS, I comment out the if while I’m working and re‐open Xcode.

It would be nice to have watchOS automatically evade tests in a similar way to how iOS evades executable targets. (Although even that seems to only work from the GUI and not command line invocations.) But as @NeoNacho said, those are the domain of Xcode, not SwiftPM.

4 Likes

Very hacky. I love it! Thank you.

For Linux, there doesn't seem to be a "TARGET_LINUX", but since my other platform is iOS, I'm using:

if ProcessInfo.processInfo.environment["TARGETING_IOS"] == nil {
  // print("ProcessInfo.processInfo.environment: \(ProcessInfo.processInfo.environment)")
  package.targets.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
  package.products.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
}

Unfortunately that doesn't actually work on iOS. Oh. I wan't quite understanding how much of a hack this is. I'm now using:

if let platform = ProcessInfo.processInfo.environment["SWIFT_PLATFORM"], platform.hasPrefix("ubuntu") {
  //print("ProcessInfo.processInfo.environment: \(ProcessInfo.processInfo.environment)")
  package.targets.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
  package.products.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
}

when running on Ubuntu, this is in the environment: "SWIFT_PLATFORM": "ubuntu16.04"**
And since I'm targeting only ubuntu and iOS, this works ok for now.

Actually, this seems better:

#if os(Linux)
package.targets.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
package.products.removeAll(where: { $0.name.hasPrefix("SolidAuthSwiftUI") })
#endif