Pitch: [SwiftPM] Swift Language Version Per Target

Hello, Swift Evolution!

I'd like us to consider the following addition to the Swift Package Manager APIs.


Introduction

The current Swift Package Manager manifest API for specifying Swift language version(s) applies to an entire package which is limiting when adopting new language versions that have implications for source compatibility.

Motivation

Adopting new language versions at the target granularity allows for gradual migration to prevent possible disruptions. Swift 6, for example, turns on strict concurrency by default, which can have major implications for the project in the form of new errors that were previously downgraded to warnings. SwiftPM should allow to specify a language version per target so that package authors can incrementally transition their project to the newer version.

Proposed solution

Add a new Swift target setting API, similar to enable{Upcoming, Experimental}Feature, to specify a Swift language version that should be used to build the target, if such version is not specified, fallback to the current language version determination logic.

Detailed design

Add a new swiftVersion API to SwiftSetting limited to manifests >= 6.0:

public struct SwiftSetting {
  // ... other settings
  
  @available(_PackageDescription, introduced: 6.0)
  public static func swiftVersion(
      _ version: SwiftVersion,
      _ condition: BuildSettingCondition? = nil
  ) -> SwiftSetting {
     ...
  }
}

Security

New language version setting has no implications on security, safety or privacy.

Impact on existing packages

Since this is a new API, all existing packages will use the default behavior - version specified at the package level when set or determined based on the tools version.

Alternatives considered

Add a new setting for 'known-safe' flags, of which -swift-version could be the first. This seems less user-friendly and error prone than re-using SwiftLanguageVersion, which has known language versions as its cases (with a plaintext escape hatch when necessary).

13 Likes

I think this is an important pitch, because incremental migration per-target is critical for the Swift 6 migration, including for packages. I have a few minor comments/questions

Should we call this swiftLanguageVersion since the term "Swift version" is ambiguous between "language" and "tools"?

It's worth clarifying what the default language version actually is when using Swift 6.0 tools, because I think it's debatable. Personally, I don't think the default when using Swift 6.0 tools should be Swift 6 mode - that's not the default in the compiler, because otherwise source compatibility won't be maintained for existing code. I can see an argument that maybe the SwiftPM behavior should be different because you need to explicitly change the package manifest to require 6.0 tools, but I also don't think using 6.0 tools is an indication that the package wants to use the Swift 6 language mode by default.

I agree with the choice to use SwiftLanguageVersion in the API here. Will you also add a v6 static let? e.g.

    /// Swift language version 6.
    public static let v6 = SwiftLanguageVersion(uncheckedString: "6")
3 Likes

I’ll push back on this: if the default mode is not Swift 6 based on a 6.0 PackageDescription, then every new package will have to specify Swift 6 mode multiple times. Certainly new versions need to not break source compatibility, but it’s also nice when the “default” settings are the ideal ones for a user who doesn’t even know there’s an important setting to change. Maybe PackageDescription versions shouldn’t always be treated as “epochs” like this that change defaults, but one with a major version number in it probably wouldn’t surprise anyone.

7 Likes

I don't think that's a given. We could change swift package init for 6.0 tools to specify the language version to be Swift 6 for the entire package, not using this new per-target API.

Should we call this swiftLanguageVersion since the term "Swift version" is ambiguous between "language" and "tools"?

Sounds good!

I agree with the choice to use SwiftLanguageVersion in the API here. Will you also add a v6 static let? e.g.

This a actually my mistake we have multiple SwiftVersion in different places and one SwiftLanguageVersion. I modified the text to use SwiftVersion enum from LanguageStandardSettings and yes, I'll add new v6 case for it!

1 Like

True, though that still ends up being two places. I personally don’t tend to use swift package init; is it assumed that most people do?

Do you think it would be feasible to include C and C++ versions with this change?

This would be going down a fragile path as Swift and SPM continue making progress toward explicit modules. In an explicit module build, the C/C++ language mode must be the same between any two modules that are connected in the dependency graph (unless you use something like @_implementationOnly import to prune the subgraph under a Swift module, but that isn't fully supported yet).

The current behavior today is that 5 is used in that case, see ToolsVersion.swiftLanguageVersion. I suspect that's only because we haven't updated it though. IMO a swift-tools-version: 6.0 package should use 6 as its language version by default - a top level swiftLanguageVersion can always be used to specify 5, with the proposed target level to update specific targets to 6 for migration purposes.

That would then mean that the default makes sense for the majority of users and those wanting to migrate have the means to do so.

3 Likes

My use case is typically say a C89 library used in a C11 or C23 executable, I currently split them into different packages.

I've also had similar cases with C89 libraries and a more modern C wrapper for a Swift interface, which I'm also splitting into different packages.

I was hoping this could help me clean that up a little!

(I often use SPM with C-only projects so I guess my use case isn't the most common!)

1 Like

That sounds like a very significant problem that needs to be fixed? Anything that makes it so that two unrelated packages can trivially conflict with each other is going to make things very brittle.

I agree adding this as a per-target Swift setting that is not an 'unsafe flag' is important for the transition to Swift language version 6.

But I also think it's reasonable to expect if swift-tools-version is a 5.x value that the default Swift language version would be 5 (as it currently is) and that updating the swift-tools-version in the manifest to 6.0 would use Swift language version 6 as the default. (I also would be completely surprised if making a new package today compiled using Swift language version 4.2 or 4)

A developer would need to explicitly update the manifest to swift-tools-version: 6.0 to change the default, so without making that change, existing packages would continue to build using Swift language version 5. I believe that means source compatibility would be maintained for all existing packages.

Once moving to swift-tools-version: 6.0 all targets could continue to be built with Swift language version 5 using the existing package-level argument:

    swiftLanguageVersions: [ .v5  ]

And then, as a target is migrated, it would add the proposed Swift setting:

     swiftSettings: [ .swiftLanguageVersion(.v6) ]

And then, once fully migrated, all of these could be removed from the manifest.

I think this allows for source compatibility for existing packages and then no specification required after migration, while giving precise control using a fairly small number of manifest changes during migration.

(Unrelated to the pitch: I also wonder if it might be possible if during a transition period (maybe for the first year after 6.0 is released?) swift package init might ask interactively which language version, 5 or 6 is desired?

If 5, the swiftLanguageVersions: [ .v5 ] could be automatically added to the generated manifest.)

11 Likes

It also occurred to me that, of course, a developer could alternatively:

  • Move to swift-tools-version: 6.0
  • Not add a package-level language version setting
  • Add swiftSettings: [ .swiftLanguageVersion(.v5) ] to each target
  • Migrate each target and remove the per-target setting

The combination of this proposed setting and upcoming feature flags really gives an amazing amount of granular flexibility in migrating.

Thinking back on the Swift 3 and Swift 4 migrations, the Swift ecosystem has come a long way!

2 Likes

+1 on the general idea of the pitch. It's important to have per-target control over the Swift language version.

Apologies if this isn't the thread to debate this point, but I find it very surprising that the default language version of the Swift 6.0 compiler is not Swift 6. Is this the plan for the actual Swift 6.0 release?

Does this mean that going forward, we'll have to pass -swift-version 6 to all CLI swift/swiftc invocations if we want to use Swift 6 features? I would find that very odd. Or is there a plan to change the default in a later Swift 6.x compiler release? (which would also be odd)

3 Likes

I’ll not elaborate the points already made on why it would make sense to default the language version to the toolchain version, but just add two more minor point in favor of that:

  1. If we want people to migrate to the latest language version defaulting to it is crucial over time as we’d expect usage of swift to grow
  2. More importantly, it would set a strange precedent for future major language versions. If we’d default to swift 5 for the swift 6 toolchain, what should be the default for the swift 7 toolchain (and why?). Much clearer to link those and provide the escape hatches for migration outlined above.
3 Likes

Every single SPM package I've created the last few years (many hundreds of them, since it is what I use as "playground") I have created with swift package init - so I'm one data point (weighted with many packages! :slightly_smiling_face: )

2 Likes

I'm happy to discuss what lead to keeping the default at the Swift 5 language mode and the implications of that, but it's not really related to this pitch so I think we should start a new discussion thread.

2 Likes

Interesting - trailing targets opt-in by removing the restriction. But that means they can't build with 5.x toolchains.

The proposal limits swiftLanguageVersion(...) to 6.0+ manifests, but would it help if >5.10 tools supported it as well?

(Assuming that's in scope for this proposal and that >5.10 is possible...)

With @available() we can selectively make new API's unavailable in older language contexts. But still when library developers want to upgrade to 6.x but still support 5.x builds, they're forced to branch, create and release different versions, and all clients are forced to manually update dependencies to pick up the new version.

An alternative way is to have a separate "libv6" target that can be incrementally adopted (even automatically via indirection if the API is compatible). In this case, tools <6.0 but >5.10 would ignore targets with .swiftLanguageVersion(.v6), but v6+ toolchains could build them.

Given 4 specifications (?: 2 optional):

Locus Specification
toolchain (which swift)
Package.swift // swift-tools-version: N.N
package ? swiftLanguageVersions: [ .vN ]
target ? swiftSettings: [.swiftLanguageVersion(.vN)]

where:

  • unstated target defaults to package
  • unstated package defaults to Package.swift
  • Package.swift <= toolchain (??)

Supporting target.v6 in 5.10+ toolchain adds two cases:

Use-case ToolChain Package.swift package target
defaults 4-6 4-6 v4-6, -- --
leading target 6 6.0 .v5 .v6
trailing target 6 6.0 .v6, -- .v5
aspiring target 6 5.10.1 -- .v6
5.x compatible >= 5.10.1 5.10.1 -- .v6 - ignored

Note

  • -- means unstated

Issues:

  1. From the SCM standpoint this means the build uses different code for the same SCM coordinates (package+target+version). That's troubling, but Swift does that already with @available and the target triple (and debug vs. release).

  2. For explicit clients of target libv6 accidentally building with 5.10.1 toolchain, we'd prefer a clear error that they need a 6.x toolchain.

I created a new thread for that:

2 Likes

My understanding is a package with an existing Package.swift file using a 5.x tool version can add a Package@swift-6.0.swift manifest to be used with Swift tools 6.0 and later.

The correct manifest will be used based on the toolchain version used to build the package.

To help avoid needing completely different branches to support different language mode versions, you can also use conditional compilation using #if swift(<6) or #if swift(>=6) to allow different code to be compiled depending on the Swift language version.

I don’t know if these are fully able to address the issues you describe in your post.

1 Like