SPM shared targets files use case, what's the alternative

I'm writing a Bluetooth library around CoreBluetooth, to create a solid code coverage I'm using as a Dependency this library.
This library requires that you substitute each import CoreBluetooth to import CoreBluetoothMock and since the mock are protocol based I could not use some functionalities. To avoid compiler errors I've created a new Xcode configuration that defines a TEST flag and I've set up the scheme to use this configuration duration using testing. Thus I have some part of my code wrapped around #if #endif including import statements.
Using the SPM and the option -Xswifc "-DTEST" I'm able to safely run unit tests.
I would like that my library won't come out with this dependency that is used only for CI and I've figured out that setting my SPM file into some like that, I could provide a target user library, a target library with that dependency compiled with that flag defined and test target that depends on this last one.
Unfortunately this configuration led me to the

error: target 'LittleBlueToothForTest' has sources overlapping sources:

I've read that SPM is made that way so my solution it doesn't seem to be applicable. I've read different post on the forum.
Here my Package file:

let package = Package(
    name: "LittleBlueTooth",
    platforms: [
        // Add support for all platforms starting from a specific version.
        .macOS(.v10_15),
        .iOS(.v13),
        .watchOS(.v6),
        .tvOS(.v13)
    ],
    products: [
        // Products define the executables and libraries produced by a package, and make them visible to other packages.
        .library(
            name: "LittleBlueTooth",
            targets: ["LittleBlueTooth"]),
        .library(
            name: "LittleBlueToothForTest",
            targets: ["LittleBlueToothForTest"])
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
         .package(name: "CoreBluetoothMock",
                  url: "https://github.com/enricodk/IOS-CoreBluetooth-Mock.git",
                  .branch("multiplatform")),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "LittleBlueTooth",
            dependencies: []),
        .target(
            name: "LittleBlueToothForTest",
            dependencies: ["CoreBluetoothMock"],
            path: "./Sources/LittleBlueTooth",
            swiftSettings: [.define("TEST")]
        ),
        .testTarget(
            name: "LittleBlueToothTests",
            dependencies: ["LittleBlueToothForTest","CoreBluetoothMock"])
    ]
)

Has someone any idea/suggestion about how can I solve that issue?

Hi! I'm not sure there actually is a solution. Ideally, you would remove the mock target and library and be able to set the TEST flag on your LittleBlueTooth target. What you would need is something like .define("TEST", condition: .when(configuration: .test)), but BuildConfiguration only has .release or .debug. Short of suggesting a change in SPM (and I'm not sure a "test" configuration is something that exists), if this is only for CI and code coverage, you could actually define two separate manifests!
Package.swift would be the one without the mock dependency and something like Package-CI.swift would have it. The CI manifest would also add the TEST flag to your LittleBlueTooth target and those would be the only differences. Your test target would in both cases depend on LittleBlueTooth. Then, in your travis.yml or whatever CI you're using you would add a step that temporarily renames Package-CI.swift. If that can be done, CI would do tests and code coverage with the mock dependency, but the distributed PAckage would not contain it.

1 Like

Hello Jonas,
yes I think that renaming the package manifest could be a viable options, I'm using GitHub Actions. I will give a try, but it's a pity that an operation like that would require this kind of trick.
I thought that I was missing something or doing something wrong at design level.
Thank you so much for your suggestion.
Andrea

I think you can use symlinks to avoid the overlapping sources error.

3 Likes

Hello Boris,
It does work, I gave a first try by doing aliases using the finder and it gives me error.
Then I've made symlink using the terminal and it worked.
Thank you a lot,
andrea

Hi @NeoNacho, and sorry for bumping this thread :see_no_evil:

However I thought it was better to have your opinion before starting a new thread.

Even though the workaround to use a symlink works (essentially by tricking SPM to see a different path :ghost:), it's a bit "sneaky" and I think that this perhaps deserves a better, more explicit solution. I see some scenarios where having multiple "groups" of targets/products can be useful, like:

  1. a product that has multiple compilation "paths" like the example in the thread (same code, compiled with or without a flag)
  2. a product made available as variable module configurations: e.g. a single module (import Foo) vs multiple submodules (import FooBar, import FooBaz, ...)

Point 2 can arguably be seen as a bit of an anti-pattern, but it can still make sense on some cases, like libraries that are transitioning from single module to multi-module and want to provide backwards compatibility or when you simply want to allow multiple setups. I see point 1 as something that can be quite frequent and a legitimate use case.

I understand and agree with the reasoning for enforcing path exclusivity (avoid invalid combinations of imports that could lead to duplicate symbols), but would it make sense to try and have the best of both worlds and be able to create product/target groups that would be mutually exclusive between themselves? Something like:

let package = Package(
    name: "Foo",
    products: [
        .library(
            name: "FooSingle",
            targets: ["Foo"],
            group: "Single" // <-- group tag
        ),
        .library(
            name: "Foo",
            targets: ["Bar", "Baz"],
            group: "Multi" // <-- group tag
        ),
        .library(
            name: "FooSingleFlag",
            targets: ["FooFlag"],
            group: "Flag" // <-- group tag
        ),
    ],
    targets: [
        .target(
            name: "Foo",
            dependencies: [],
            path: ["Sources"],
            group: "Single" // <-- group tag
        ),
        .target(
            name: "Bar",
            dependencies: [],
            path: ["Sources/Foo"],
            group: "Multi" // <-- group tag
        ),
        .target(
            name: "Baz",
            dependencies: [],
            path: ["Sources/Baz"],
            group: "Multi" // <-- group tag
        ),
        .target(
            name: "FooFlag",
            dependencies: [],
            path: ["Sources"],
            group: "FooFlag", // <-- group tag
            swiftSettings: [.define("FLAG")]
        ),
    ]
)

This new group key would allow SPM to perform the exact same checks on path exclusivity (now per group), while allowing multiple groups to co-exist and still preventing users from invalid configuration by forbidding products/targets imports from different groups simultaneously. It's also a purely additive change (I think?), which is always good.

What do you say? If you think this idea is worth exploring I can move this discussion into a proper pitch.

Thanks! :pray:

I think this sounds like a reasonable pitch to me.

I'm assuming the idea would be that SwiftPM enforces that only one target from a group can be present in the dependency closure of each product? That would continue to avoid any issues with duplicated symbols and the like.

I think this sounds like a reasonable pitch to me.

Great! :raised_hands:

I'm assuming the idea would be that SwiftPM enforces that only one target from a group can be present in the dependency closure of each product?

Perhaps I misunderstood your question, but with this approach a product could still have multiple targets as dependencies, as long as all of them belong to the same group (e.g. Foo can still be composed by Bar and Baz, as both belong to group "Multi"). The sanity of this would be ensured by all targets in a group being subject to the current path exclusivity checks, but scoped to their own group

Extra checks will be required to ensure that all imported products from a package belong to the same group when defining dependencies (e.g. I can't import both Foo and FooSingle into another package Potato), precisely to avoid duplicated symbols as you mention :ok_hand:

Hope this made it clearer :grinning_face_with_smiling_eyes: