Update SPM to support custom configuration names

The Problem

Currently SPM only lets you have Debug and Release configurations.

The problem is, in Xcode, you can have any number of configurations and you can name them whatever you want. For example one app that I worked on had the following three configurations: "Production", "Staging", and "Dev". The current app I work on has "Debug", "Release", and "Testing."

So it's really problematic that Swift packages only support "Debug" and "Release."

Example From Our App

In our app we have an "AllUnitTests" scheme that incorporates all the unit test targets in the workspace, including some that are Swift packages.

However, because AllUnitTests scheme builds with an Xcode configuration called "Testing," this causes the Swift packages to all get built in their "Release" configuration. I've been told this happens because some heuristics code maps configurations called "Testing" to SPM's "Release" configuration. I don't know whether that's intended or just a fallback behavior, but the end result is that when Swift packages' test targets are built under our "Testing" configuration, @testable import doesn't work on any of the tests for that package, because SPM doesn't enable testability for Release configurations.

The Swift packages get built without testability enabled even though our AllUnitTests.xcscheme itself has:

         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "NO"
            buildForProfiling = "NO"
            buildForArchiving = "NO"
            buildForAnalyzing = "NO">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "MySwiftPackage"
               BuildableName = "MySwiftPackage"
               BlueprintName = "MySwiftPackage"
               ReferencedContainer = "container:Packages/MySwiftPackage">
            </BuildableReference>
         </BuildActionEntry>

One would think that, regardless of the name of the configuration, buildForTesting = "YES" ought to be enough to clue Xcode to, y'know, enable testability on the target being built. But I realize that "build for testing" and "enable testability" are two different concepts that just sound similar, and there might be times when you want to build a release configuration for testing.

Pitched Solution(s)

We need a solution where Xcode configurations with custom names will play nice with Swift packages. Ideally it would allow a Swift package to inherit build settings from a shared .xcconfig file.

Firstly we'd need a way to create a BuildConfiguration with a custom name, e.g.:

BuildConfiguration.custom("Testing")

... so at a bare minimum you could do:

targets: [
    .target(
        name: "MyPackage",
        dependencies: ["SomePackage", "SomeOtherPackage"],
        swiftSettings: [
            .define("ENABLE_TESTABILITY", 
            .when(configuration: .custom("Testing"))
        )
    )
]

This would be good for one-offs but it wouldn't scale very well in a large project with 50+ modules like our application. What we really need before we can feasibly transition away from all our modules existing as Xcode frameworks, is a way to share configurations across multiple Swift packages using xcconfig files or something equivalent to them, e.g.:

    targets: [
        .target(
            name: "MyPackage",
            dependencies: ["SomePackage", "SomeOtherPackage"],
            xcconfigs: [
                XCConfig(
                   path: "../../SharedConfigs/Testability", 
                   config: .custom("Testing")
                ) 
            ]
        )
    ]

This would be ideal for our circumstance because we currently keep all our build settings defined in a centralized place so that individual teams making new modules don't have to reinvent any wheels and it's easy to adjust the settings used by all modules in a single place rather than having to edit a bunch of separate files and hope you didn't miss one.

We sorely want to use Swift Packages for all of our monorepo's modules because Xcode project files (with their random GUIDs for everything) are a common source of git merge conflicts, and often become corrupt due to people not resolving those conflicts correctly. To never have to deal with Xcode project files ever again would make everyone's lives 1000% better, but the lack of flexibility in how SPM deals with build configurations is currently a blocker to us making this transition because we can't convert all modules to packages in a single PR and it's also impractical to change our custom configuration names.

Counter-arguments and Counter-counter-arguments

The proponents of the current design have said supporting custom configuration names would be bad because, if some public package uses some weird custom configuration name, then you might not be able to use it in your app without also adopting that custom name.

However I think it should be up to the author of a Swift package whether to use the "standard" names or whether to support custom configuration names, because not all Swift packages are public—many of them are private packages used merely as the easiest, simplest way to modularize code within an Xcode project. And maybe there are configurations we'd like consumers of a package not to use, like ones geared towards package development and testing, as opposed to debugging and release.

Also, I don't think that library authors would abuse custom configuration names, because authors of public libraries are smart enough to know not to do that, as evidenced by the fact that we haven't had this problem up to now with libraries that were published as Xcode framework projects. In 10 years I've never seen a public library that was incompatible with a project I've worked on due to using a weird config name.

So I don't think it's a rational fear to say that supporting more than just two configurations would cause problems.

Closing

Please let me know how we can get this pitch into a proposal and move forwards. Thanks!

4 Likes

You're spot on that this is a sorely missing feature of SwiftPM, but I think this is the wrong direction because it's explicitly relying on XCConfigs which are tied to Apple and Xcode.

SwiftPM should be completely agnostic to the platform and tooling used outside of the Swift toolchain.

What I think instead should be done is we "refactor" some of the Target configuration into a new TargetConfiguration protocol which the Package.swift manifest will define:

protocol TargetConfiguration:
  CaseIterable,
  [RawRepresentable where RawValue == String | CustomStringConvertible]
{
  // `description` or `rawValue`, depending on the protocol inheritance
 
  // dependencies, exclude, sources, resources, cSettings, cxxSettings, swiftSettings, linkerSettings
  // exactly as like the current `.target(...)` factory method supports

  var isReleaseConfiguration: Bool { get }
}

then we have the ability to do the following in Package.swift:

import PackageDescription

enum Configurations: TargetConfiguration {
  case prod, stage, dev

  var isReleaseConfiguration: Bool { self == .prod }

  var swiftSettings: [SwiftSetting]? {
    guard self != .prod else { return nil }
    return [.define("ENABLE_TESTABILITY")]
  }
}

let package = Package(
  // ...
  targets: [
    .target(
      // ...
      configurations: Configurations.allCases
    )
  ]
)

SwiftPM could then expose a StandardConfigurations conformance that is used by default that we have today: Release and Debug.

2 Likes

I like how simple protocol TargetConfiguration is, and would love to see it working in action.

As for the original pitch, I strongly oppose any Xcode-specific additions to Package.swift like XCConfig pitched by the OP. Swift and SwiftPM already support too many Xcode-specifc and Apple-specific features, while neglecting support for other platforms. It would be very unfortunate if it moves even further in that direction.

8 Likes

As for the original pitch, I strongly oppose any Xcode-specific additions to Package.swift like XCConfig pitched by the OP. Swift and SwiftPM already support too many Xcode-specifc and Apple-specific features, while neglecting support for other platforms. It would be very unfortunate if it moves even further in that direction.

OK that makes sense to me. I totally support the alternative proposed by @Mordil. Seems like Apple would then just compare the rawValue of each TargetConfiguration case to its known Xcode configuration names, and use the one that matches most closely.

For example if there is a custom Xcode configuration called "Testing" and a given package has .testing in its custom TargetConfiguration then Xcode would map this case to "Testing" configuration.

Sounds like a plan to me!

1 Like

What needs to happen in order for this to move forwards with some degree of expediency?

As a first step, someone would most probably need to submit a PR against the SwiftPM repo and write a formal proposal draft for it. Depending on feedback, those may be considered for Swift Evolution review.

Looking at that repo now. Happy to make a PR but first I'll need to familiarize myself with the current SwiftPM code.

Firstly, where in this repo should I look to find the current heuristic that maps a given configuration name from an Xcode project, to ".release" or ".debug"? Seems like a good first step would be to document and possibly improve this heuristic, before we worry about adding additional cases.

SwiftPM integration that allows adding packages as dependencies is entirely up to Xcode, and is proprietary. The only open-source part is the existing swift package generate-xcodeproj subcommand, which is deprecated and is not relevant here, if I understand correctly.

Because of this, and also the fact that this proposed feature should be cross-platform, it might be better to implement support for it in package manifests first, and then the build system, and the CLI interface. Then it's up to Xcode if they implement support for it or not on their side.

Let me see if I understand correctly.

There is some undocumented "heuristic" that Xcode uses to decide whether to map a given configuration to SPM's .debug or .release configuration. But that heuristic is proprietary to Xcode and so all SPM can do is to start supporting custom names and then (hopefully) Xcode uptakes that change.

Is that accurate?

That is my understanding, maybe other folks (particularly those working on SwiftPM or Xcode) could clarify it.

OK thanks Max. Also what specifically did you mean by "the build system" and "the CLI interface"? Are those separate repos from SwiftPM or part of it?

I really wish I knew how their "heuristic" works because in the short term we need a way to have a debug and a testing configuration, regardless of what they're called...

There's a separate repository for llbuild, which is what SwiftPM currently uses under the hood. But the SwiftPM repo itself contains some build system logic. Namely, it transforms package manifests into llbuild rules. I guess build configuration flags may be passed somewhere across that boundary.

As a side note, through experimentation I discovered that prepending the name of an Xcode configuration with "Debug_" will cause Xcode to build Swift packages in their Debug configuration.

So for example, after renaming our "Testing" configuration to "Debug_Testing", now all the packages are built with testability enabled.

Having a workaround like this means our need for SPM to support custom package names is greatly diminished.

Still, I will see if I can find time to make PRs to the Swift repos to add support for other configuration names, since there are other aspects of our "Testing" configuration that are different besides the fact that Testability is enabled.

1 Like
Terms of Service

Privacy Policy

Cookie Policy