Conditional compilation using traits

I have the following case for which I’m trying to use package traits.
I have a library of which some part of the implementation is platform specific. I've moved that part separate targets (A and B). Both of those implementations provide a single public function with the same signature. So that if either trait A is active or trait B is active SomeLibrary will be able to compile.

Now the below setup works perfectly if I just instruct swift build to build the SomeLibrary target by using:
swift build --target SomeLibrary --traits PlatformA
But fails when I simply try to build the full package using:
swift build --traits PlatformA
since this will still try to build PlatformBImplementation (which fails on my current platform)

Is there any way to only add this target when a certain trait is active?

A different solution would be wrapping everything in PlatformBImplementation in #if PlatformB but that seems to kind of defeat the purpose.
Another solution would be to have both implementations live in a separate package. But it seems kind of overkill in my situation.

I'd really like to be an able to do this using traits. Or understand why (if true) this is not possible.

Any help is greatly appreciated!

let package = Package(
    name: “some-package”,
    products: [.library(name: “SomeLibrary”, targets: [“SomeLibrary”])],
    traits: [
        “PlatformA”,
        “PlatformB”,
    ],
    targets: [
        .target(
            name: “SomeLibrary”,
            dependencies: [
                .target(name: "PlatformAImplementation", condition: .when(traits: ["PlatformA"])),
                .target(name: "PlatformBImplementation", condition: .when(traits: ["PlatformB"])),
            ]
         ),
         .target(name: “PlatformAImplementation”),
         .target(name: “PlatformBImplementation”),
    ]
)

That's unfortunately the case caused by the fact that swift build builds the whole package by default. We had to do this in WasmKit for the WasmDebuggingSupport trait, with additional #if !os(Windows) platform exclusions in Package.swift, which are not compatible with cross-compilation unfortunately.

@FranzBusch WDYT of new .when conditions on target declarations that would tell SwiftPM which targets to exclude from the package graph completely when swift build doesn't have any target/product subsets specified via --product or --target?

@JaapWijnen would you mind filing an issue on SwiftPM in the meantime? No matter how we resolve it, it definitely feels like a severe limitation that should be recorded in the bug tracker.

I think we have to differentiate between building such a package as a root package vs a dependency of another package. If you build a root package it is expected that you build all targets of the package unless you specify a specific target. This is in my opinion orthogonal to traits. When your package is used as a dependency though then only the targets enabled by traits should actually be built.

I don't know what that .when condition would be here. What you are describing here is target based dependency resolution which is currently not fully implemented but should work for trait guarded targets in the 6.2 release. @JaapWijnen can you try out using your package as a dependency, enable one of the two traits, and check that a simple swift build is only building the intended target?

@FranzBusch yes this works. I've worked around the issue as a whole by having sub packages inside my current package. So I can depend on those when certain traits are active.

But the whole thing would be much simpler if I could do this at a target level. Since then all code could live in the same package.
The implementations which now live in a sub package are not something that should be living in a package in their own repo as they are details of the main package I'm trying to create.
So the whole dance of putting them in separate packages in the same repo adds quite some boilerplate and doesn't help in understanding how everything fits together as well.

Does that make sense?

Created an issue here: include target in build when trait is active · Issue #9340 · swiftlang/swift-package-manager · GitHub
I simply added the same use case description.

Like @FranzBusch mentioned, building for a root package would result in building all targets defined in your root when executing a swift build.

I expanded on this and how traits can conditionally include transitive dependencies on another forum post here if it’s of any interest, but the TLDR is that SwiftPM doesn’t currently handle target-based resolution by default. I’m interested in looking into this area in the near future, but for now I’ve done some work on an experimental feature that you can enable with --experimental-prune-unused-dependencies . Since it is experimental, however, I would proceed with caution :slight_smile:

1 Like

That's super interesting! Thanks for pointing me to the discussion as well.

I think an unused target analysis would be interesting for a top level swift build call.
Regardless though. Is there a specific argument against the following in parallel to that functionality?:

let package = Package(
    name: "package",
    traits: ["A"],
    targets: [
        .target(
            name: "Library", 
            dependencies: [.target(name: "A", condition: .when(traitsEnabled: ["A"])]
        ),
        .target(
            name: "A",
            condition: .when(traitsEnabled: ["A"]),
        ),
    ]
)

Which would effectively resolve to the following when running swift build --traits A

let package = Package(
    name: "package",
    targets: [
        .target(
            name: "Library", 
            dependencies: ["A"]
        ),
        .target(name: "A"),
    ]
)

And to the following when running swift build (trait A not being active)

let package = Package(
    name: "package",
    targets: [
        .target(name: "Library"),
    ]
)
1 Like

This has come up in a number of contexts. I’ve mainly seen it with platform specific targets, e.g. an executable target that is specific to the Raspberry Pi Pico shouldn’t be built for my host platform.

I think it’s common enough we should allow when conditions on targets (I am slowly thinking products are just targets in their own right, but that’s for another post) the same as we do with build settings and dependencies.

Right now my understanding is that our build systems don’t support conditionals on targets but SwiftPM is the one that generates the “build all the targets” target so we could create separate ones for the given set of conditions and call for the build of the right target based on the enabled conditions.

I’m writing up a vision document for the things people would like to see in SwiftPM’s next phase of evolution. I’ll make sure I add it there.

4 Likes