Search code examples
xcode11swift-package-manager

Copying Resource Files For Xcode SPM Tests


I am new to the Swift Package Manager but with its integration into Xcode 11 it is time to give it a try. I have a new application and SPM library within a new workspace. I have a working library with tests and have successfully imported the library into the application.

I need to extend the SPM library with new tests that parse json files. I have learned that a resources directory feature is not supported. The only workable scheme seems to be a file copy step added to the library build process so that the resource files can be discovered by the executable.

I could figure out how to do this from the command line but not with Xcode running the build and test. There is no Copy Bundle Resources, build phase for swift packages. In fact everything appears to be hidden by Xcode.

I have looked within the SPM for Makefile type files that would allow me to edit default command line actions thereby circumventing Xcode; but I am not seeing them.

Is there some way to interact/control how Xcode 11 builds SPM targets so that I can copy non-code files to test targets?


Solution

  • This is another workaround to provide access to test resources. Hopefully an answer to the OP's question will be forthcoming.

    Using the code below, an extension is created to allow callers to create URL's to test resources like this.

    let url = URL(forResource: "payload", type: "json")
    

    This code requires that all resource files be located in a flat directory named "Resources" just under the test target.

    // MARK: - ./Resources/ Workaround
    // URL of the directory containing non-code, test resource fi;es.
    //
    // It is required that a directory named "Resources" be contained immediately below the test target.
    // Root
    //   Package.swift
    //   Tests
    //     (target)
    //       Resources
    //
    fileprivate let _resources: URL = {
        func packageRoot(of file: String) -> URL? {
            func isPackageRoot(_ url: URL) -> Bool {
                let filename = url.appendingPathComponent("Package.swift", isDirectory: false)
                return FileManager.default.fileExists(atPath: filename.path)
            }
    
            var url = URL(fileURLWithPath: file, isDirectory: false)
            repeat {
                url = url.deletingLastPathComponent()
                if url.pathComponents.count <= 1 {
                    return nil
                }
            } while !isPackageRoot(url)
            return url
        }
    
        guard let root = packageRoot(of: #file) else {
            fatalError("\(#file) must be contained in a Swift Package Manager project.")
        }
        let fileComponents = URL(fileURLWithPath: #file, isDirectory: false).pathComponents
        let rootComponenets = root.pathComponents
        let trailingComponents = Array(fileComponents.dropFirst(rootComponenets.count))
        let resourceComponents = rootComponenets + trailingComponents[0...1] + ["Resources"]
        return URL(fileURLWithPath: resourceComponents.joined(separator: "/"), isDirectory: true)
    }()
    
    extension URL {
        init(forResource name: String, type: String) {
            let url = _resources.appendingPathComponent("\(name).\(type)", isDirectory: false)
            self = url
        }
    }