How to declare general build settings in Package.swift

We have a project that supports both iOS and tvOS. We have common code and code per platform. To make everything work we are using EXCLUDED_SOURCE_FILE_NAMES and INCLUDED_SOURCE_FILE_NAMES build settings for [sdk=appletv*] and [sdk=iphone*] respectively (4 combinations). Our development flow requires that we expose our code as a local swift package.

Looking at the Package syntax, I found the following drawbacks:

  1. We have the sources and exclude properties on Target, but they can't be customized per sdk.
  2. We're using the build settings such that we first exclude files and then include some of them back. Afaik you can't do that on Target.
  3. There are some conditional c, cxx, swift and linker settings inside Target but the above build settings don't fall in either category.

I guess one workaround could be to have 2 separate targets for the tv and phone platforms.

But question still holds - is there a way to specify generic build settings inside Package.swift? And are the statements of the above drawbacks correct?

There are two approaches that I could recommend that would solve your problem, both of which involve forgetting about manual source file includes and excludes.

Solution 1

Use conditional compilation and file naming pattern to compose a multi-platform module that will work equally well regardless of build system limitations:

MyFeature~iOS.swift:

#if os(iOS)
    /* ... */
#endif

MyFeature~tvOS.swift:

#if os(tvOS)
    /* ... */
#endif

Pros:

  • Easy to set up and scale up with support for more platforms.
  • Very flexible, can have code that is common between some of the platforms, but not all.

Cons:

  • Noticeable boilerplate.
  • Large multi-purpose module.

Solution 2

Split the single module into 4 pieces, subdividing their responsibilities, and allowing each piece to be built only when necessary:

Package.swift
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
    name: "my-library",
    products: [
        .library(
            name: "MyLibrary",
            targets: [ "MyLibrary" ]
        )
    ],
    targets: [
        .target(
            /* The umbrella module (client code only uses this) */
            name: "MyLibrary",
            dependencies [
                .target(name: "_MyLibraryCore"),
                .target(name: "_MyLibraryAspect_iOS", condition: .when(platforms: [.iOS])),
                .target(name: "_MyLibraryAspect_tvOS", condition: .when(platforms: [.tvOS])),
            ]
        ),
        .target(
            /* The core module (platform-agnostic functionality) */
            name: "_MyLibraryCore"
        ),
        .target(
            /* The iOS aspect module (iOS-specific functionality) */
            name: "_MyLibraryAspect_iOS",
            dependencies: [
                .target("_MyLibraryCore")
            ]
        ),
        .target(
            /* The tvOS aspect module (tvOS-specific functionality) */
            name: "_MyLibraryAspect_tvOS",
            dependencies: [
                .target(name: "_MyLibraryCore")
            ]
        )
    ]
)

Sources/MyLibrary/MyLibrary.swift (the only file in the umbrella module):

@_exported import _MyLibraryCore
#if os(iOS)
    @_exported import _MyLibraryAspect_iOS
#endif
#if os(tvOS)
    @_exported import _MyLibraryAspect_tvOS
#endif

Pros:

  • Smaller, more focused modules.
  • No extra boilerplate.

Cons:

  • Takes some effort to set up scale up with more platform support.
  • Not flexible, every platform-specific chunk of code is isolated.

Thank you @technogen for the detailed solutions. They seem promising. I'll try them out.

1 Like