Pitch: mutually exclusive groups of targets

Currently, SPM performs checks to ensure no overlapping source paths exist among all targets in a package. Otherwise, the following error is shown:

error: target 'Foo' has sources overlapping sources:

This makes complete sense, because otherwise it would be very easy to have issues such as duplicated symbols. However, there are some scenarios where having multiple targets with overlapping paths can be desirable, and even required, to support different configurations of the same package:

  1. a product that has multiple compilation "paths", e.g. same code, compiled with or without a particular 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 1 as something that can be quite frequent and a legitimate use case. This limitation is mentioned in this discussion, and was partly what sparked the idea for this pitch:

While the above problem is related to conditionally compiling a target to use in a test target, it's easy to see this being an issue for targets exposed to products (e.g. to conditionally compile a feature).

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.

Current workaround

On the above discussion we can find a workaround for this (thanks @NeoNacho :pray:), which is to use a symlink to the overlapped source path, and use that in the target that requires it. While this works perfectly in practice (essentially by tricking SPM to see a different path :ghost:), it's a bit "sneaky" and allows for duplicate symbols to happen, precisely what the path exclusivity validations try to prevent.

Proposal

That led me to think that this perhaps deserves a better, more explicit solution. In practice, we could have the best of both worlds and be able to create product/target groups that would be mutually exclusive between themselves (group scope), instead of globally (package scope). 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/tag would allow SPM to perform the exact same checks on path exclusivity (now scoped per group), while allowing multiple groups with overlapping paths to co-exist. Users would still be prevented from possibly problematic/invalid configurations by forbidding products/targets imports from different groups simultaneously (e.g. I can't import both Foo and FooSingle into another package Potato). Products from a particular group can only reference targets from the same group, enforcing the same restriction.

Finally it would be a purely additive change, because if we added a new group: String? parameter as mentioned in the example, the default value could simply be nil which would be equivalent to the "global" group, while being completely backwards compatible.

Hope this made sense! Thoughts? :grinning_face_with_smiling_eyes:

5 Likes

That is a great proposal, while the workaround works it would be great to have an official way to do that.

Terms of Service

Privacy Policy

Cookie Policy