Does SPM support colors in asset catalogs?

I have a Swift Package that contains an asset catalog, which in turn contains a color set. Then, there’s this code in the package:

import AppKit

extension NSColor {
    static var someBeautifulColor: NSColor {
        NSColor(named: "someBeautifulColor", bundle: .module)!
    }
}

The unit test then accesses the color:

final class MyColorsTests: XCTestCase {
    func testColorAccess() {
        XCTAssertNotNil(NSColor.someBeautifulColor)
    }
}

That’s it. This works fine when run from Xcode, but fails when run using swift test. It’s clear why when examining the MyColors_MyColors.bundle resource bundle in the respective build products folders:

  • When built using Xcode, there’s an Assets.car file, i.e. asset catalog got compiled by Xcode
  • When built using SPM, there’s just the original Colors.xcassets directory, i.e. SPM only copied the catalog but didn’t compile it

I explicitly mentioned the asset catalog in the Package.swift file using .process("Resources/Colors.xcassets"), but it doesn’t help.

Is there any way to tell SPM to correctly handle this or am I just out of luck here?

I am using Xcode 13.2.1, Swift 5.5.2.

Here’s a sample project that can be used to reproduce this issue:

Just a guess, but it seems likely that SPM on its own doesn't know how to process an asset catalog. Only the additional integration of the SPM support in Xcode gets you the appropriate behavior. This sort of stuff should really be lowered into SPM proper but I don't know how likely that is.

Commandline SwiftPM does not support Xcode specific resource types like asset catalogs. You can build/test packages that use them with xcodebuild on the CLI, though.

Thanks for both your answers, I suspected as much. Also thanks for the hint about xcodebuild, this should be a usable workaround in my situation!

BTW, the SPM docs for the static Resource.process method state that it “[a]pplies a platform-specific rule to the resource at the given path” – it sure would be nice to know what those are :confused:

In my experience this is because SwiftPM / XCTest have a mismatch in expectations of where resources will be moved and accessed.

I had to write this extension for exposing the resource bundle in order to get it working for unit tests:

extension Bundle
  public static var myBundle: Bundle {
    /*
     this is necessary because the way test targets are sandboxed.
     There's a misunderstanding of where bundles and assets
     are supposed to be, so during unit tests the bundle fails to initialize properly
     */

    guard
      let testBundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix("xctest") })
    else { return .module }

    let fixedBundleURL = testBundle.bundleURL.appendingPathComponent("my_module.bundle")
    guard let bundleForTests = Bundle(url: fixedBundleURL) else {
      fatalError("failed to create bundle for tests")
    }

    return bundleForTests
  }
}
2 Likes

Thanks for your input! I ran into this issue before, but this is definitely not what my original message was about.

I have code similar to yours that tries to be Bundle.module (as Packages can use implicitly), but for any module/package/target. I’m calling it Bundle.current() and it uses a defaulted #fileID argument of the caller to determine what module called the function. This func current(fileID: StaticString = #fileID) then looks at various candidates to find the resource bundle, which may be the containing bundle (usually main bundle, but also things like plug-in bundles), any of the loaded bundles (Bundle.allFrameworks), as well as some special cases for unit tests. Those use some heuristics like walking back the call stack to look for the calling module using things like Thread.callStackReturnAddresses and dladdr() (although I want to evaluate using #dsohandle instead of the call stack for that). It’s… not pretty.

The hard part is not to get the path to the binary that contains the calling function, but to get the resource bundle that is associated with that binary. I don’t quite understand why this can be all over the place, but it’s a solvable problem for a defined project like what I’m working on. Still, just last week someone added a unit test to a Swift Package that accesses the resources of another Swift Package and that resulted in a new constellation that broke Bundle.current(), so it feels like a game of whack-a-mole at times.

I finally had time to try this out and unfortunately, this doesn’t really work. There are a couple of issues here. I detailed them below, but the gist is that using xcodebuild won’t work out (or I did something wrong – always possible :see_no_evil:).

Why `xcodebuild` doesn’t work for this
  • If all you have is a Swift Package, you can’t just run xcodebuild
  • If you run swift package generate-xcodeproj, you get the following warning, which doesn’t give me confidence that this would be usable as a longterm solution.

    warning: Xcode can open and build Swift Packages directly. 'generate-xcodeproj' is no longer needed and will be deprecated soon.

  • When you ignore that warning and look at the generated Xcode project, it becomes clear that it does not include any resources. So you’d have to add them manually, which means you can’t use this in an automated script.
  • The Xcode project does not compile because Bundle.module seems to be implicitly added by SPM, but Xcode doesn’t know about it.
  • Even after adding the color catalog manually to the Xcode project and working around the Bundle.module issue, I couldn’t get it to work because the asset catalog was never in the build product. I even explicitly added a “Copy Files” build phase, but it didn’t show up. I don’t what that is about.

I guess what we would need would be for xcodebuild to support Swift Packages, just like Xcode itself does. That is, it would need to be able to correctly handle Package.swift (without an Xcode project), including the specified resources and it would have to correctly apply the Xcode-specific resource processing rules to them.

I guess the best course of action is probably to wait for Swift 5.6 and SE-0303 to land so we can invoke actool directly to compile the asset catalog. Let’s hope this will work!

xcodebuild -scheme ... should absolutely work to build standalone packages. You can use xcodebuild -list to see which schemes are available.

2 Likes

Oh! I did not realize that. Thank you for the hint, this seems to work fine.

I just assumed that didn’t work because running just xcodebuild test didn’t work (I haven’t used that command much before, so I’m no expert in its syntax) and stated that the directory “does not contain an Xcode project” – so it never occurred to me that it would work anyway.