Pitch: Ability to link Swift Packages to XCFrameworks

I'd like to pitch adding a way to link a local Swift Package to a local XCFramework anywhere on the same hard drive.

Proposed Solution

It would work like this:

let package = Package(
    name: "SwiftUISnapshotTestCase",
    platforms: [
        .iOS(.v13),
    ],
    products: [
        .library(
            name: "SwiftUISnapshotTestCase",
            targets: ["SwiftUISnapshotTestCase"]
        ),
    ],
    dependencies: [ .xcframework(relativePath: "../../Carthage/Build/FBSnapshotTestCase.xcframework")]
    targets: [
        .target(
            name: "SwiftUISnapshotTestCase",
            dependencies: ["FBSnapshotTestCase"]
         )
    ]
)

Rationale

Currently we are reduced to this kind of garbage when we want to link to an XCFramework in our Carthage folder:

// swift-tools-version:5.3

import PackageDescription

let base = "${PROJECT_DIR}/../../../Carthage/Build/FBSnapshotTestCase.xcframework"
let base2 = "${PROJECT_DIR}/../../Carthage/Build/FBSnapshotTestCase.xcframework"

let simDir = base + "/ios-arm64_x86_64" + "${LLVM_TARGET_TRIPLE_SUFFIX}"
let realDir = base + "/ios-arm64" + "${LLVM_TARGET_TRIPLE_SUFFIX}"

let simDir2 = base2 + "/ios-arm64_x86_64" + "${LLVM_TARGET_TRIPLE_SUFFIX}"
let realDir2 = base2 + "/ios-arm64" + "${LLVM_TARGET_TRIPLE_SUFFIX}"

let package = Package(
    name: "SwiftUISnapshotTestCase",
    platforms: [
        .iOS(.v13),
    ],
    products: [
        .library(
            name: "SwiftUISnapshotTestCase",
            targets: ["SwiftUISnapshotTestCase"]
        ),
    ],
    targets: [
        .target(
            name: "SwiftUISnapshotTestCase",
            swiftSettings: [
                .unsafeFlags([
                    "-Fsystem", simDir,
                    "-Fsystem", realDir,
                ]),
            ],
            linkerSettings: [
                .linkedFramework("FBSnapshotTestCase"),
                .unsafeFlags([
                    "-Xlinker", "-F", "-Xlinker", simDir,
                    "-Xlinker", "-F", "-Xlinker", realDir,
                    "-Xlinker", "-F", "-Xlinker", simDir2,
                    "-Xlinker", "-F", "-Xlinker", realDir2,
                ])
            ]
        ),
    ]
)

We have to do this ugly hack because other packages that may link to this guy, are in different levels of nesting within the work space—some are 3 levels, some are 2 levels. It's incredibly annoying to have to put an ugly hack like this, which always generates lots of superfluous warnings, just to do a simple thing like link Swift Package to an XCFramework from the Carthage folder.

Alternatives Considered

We have considered using Swift Package versions of our Carthage dependencies but the Xcode-SPM integration makes it take forever to open the workspace if your packages aren't downloaded already, and it requires all the source packages to be visible within Xcode which makes our already slow workspace run even worse and be more cluttered. Our CI system is integrated with Carthage using a custom cacheing solution that it would be non-trivial to rewrite to handle cacheing of pre-built SPM dependencies and there's no way for SPM to automatically build all the dependencies as XCFrameworks that don't have to be rebuilt every time a new version of the compiler is issued forth. So we are sticking with the tried and true Carthage for now, and are only using SPM as a way to avoid using Xcode projects for declaring local modules.

As such we don't need SPM's "safety" restrictions which are designed to prevent malicious third-party packages from wreaking havoc on your computer. All our packages are our own source-code. We just want more flexibility to use these packages the same ways that Xcode projects can be used, like being able to link them to our Carthage dependencies.

I also tried pitching the idea of creating a second kind of Swift Package called a "Swift Project" that would be for your local code, but this idea was shot down and I was told to just make the suggestions to improve Swift Packages. After awhile of considering how to boil it down to one simple request, here I am.

Thanks for your consideration.

1 Like

Did you tried:

.binaryTarget(
    name: "FBSnapshotTestCase",
    path: "./../Carthage/Build/FBSnapshotTestCase.xcframework.zip"
)

on your project?

Sorry, that does not work; you cannot have a target outside your package's folder hierarchy, because targets—including .binaryTarget—were not meant for linking a package to its external dependencies (that's what "dependencies" parameter is for). Targets are only intended to represent the modules that form up package products and its tests... and .binaryTarget is only intended for package authors like Google who want to distribute precompiled XCFrameworks wrapped in a Swift package, e.g. Google Firebase.

Meanwhile the use case of this proposal is totally different than that, if I'm understanding. For example at my org, we do not use SPM to distribute anything, nor do we use it to consume any distrubuted libraries. We purely use SPM as a way to avoid .pbxproj files when creating local modules, because .pbxproj files are a constant source of problems:

  • merge conflicts due to .pbxproj's reliance on randomly-generated GUIDs
  • build anomalies due to the tendency for new .pbxproj files to be generated with project-level overrides that have to be removed in order for the settings in our global .xcconfig files to be respected
  • lack or basic features like auto-sorting of files and folders within the project

We like the simplicity, elegance, and maintainability of Swift Packages, but we need the basic ability to link our packages to external xcframework dependencies. It's actually kind of amazing that this would even be a missing feature, but I believe this owes to SPM's developmental origins as a way for the Swift on Linux community to manage third-party dependencies, whereas many of us iOS/iPadOS developers use Carthage or CocoaPods for dependency management and do not want to use SPM for that.

We just want to use SPM for declaring local modules. However the inability of Swift Packages to link to xcframeworks outside their current folder hierarchy is a major annoyance. I hope that makes sense.