Strict Concurrency Checking in Swift Packages

How do you enable strict concurrency checking for all targets in a Swift Package?

Strict Concurrency Checking

In Swift 5.7, Apple added a build setting to control how strictly the Swift compiler checks Sendable constraints and actor-isolation. You can find the setting in the Xcode Build Settings, in the Swift Compiler - Language section:

Strict Concurrency Checking - Complete

The setting has three levels:

  • Minimal: Enforce Sendable constraints only where it has been explicitly adopted and perform actor-isolation checking wherever code has adopted concurrency.
  • Targeted: Enforce Sendable constraints and perform actor-isolation checking wherever code has adopted concurrency, including code that has explicitly adopted Sendable.
  • Complete: Enforce Sendable constraints and actor-isolation checking throughout the entire module.

The default is minimal checking which only checks when you explicitly mark something as Sendable. Apple has suggested you work towards complete checking, not just to avoid potential problems, but also in preparation for the expected checking that a future release of Swift 6 will enforce.

Using the Xcode build setting works fine for the main App bundle but how do you enable it for targets contained in a Swift package?

Swift Package Manager Settings

The Swift Package Manager lets you add build settings to a target in the Package.swift manifest file:

.target(
  name: "AppFeature",
  dependencies: ["Model"],
  swiftSettings: [
    .define("ENABLE_SOMETHING")
  ],
  linkerSettings: [
    .linkedLibrary("lib123")
  ]
),

Starting with Swift tools version 5.8 it became possible to enable both experimental and upcoming Swift language features in the swiftSettings of a Swift Package. Strict concurrency checking is an experimental feature so we can turn it on for a target as follows:

.target(
  name: "AppFeature",
  dependencies: ["Model"],
  swiftSettings: [
    .enableExperimentalFeature("StrictConcurrency")
  ]
),

I’ve become a big fan of splitting a codebase into modules so my Package.swift file looks more like this:

let package = Package(
  name: "MyApp",
    ...
    products: [
        .library(name: "AppFeature", targets: ["AppFeature"]),
        .library(name: "Feature1", targets: ["Feature1"]),
        .library(name: "Feature2", targets: ["Feature2"]),
        .library(name: "Feature3", targets: ["Feature3"]),
        ...
        .library(name: "AppModel", targets: ["AppModel"])
    ],
    ...
}

When you add the test targets that can quickly add up to a lot of targets where I need to add the swiftSettings configuration. It helps a little to define the settings up front:

// swift-tools-version: 5.9

import PackageDescription

let settings: [SwiftSetting] = [
  .enableExperimentalFeature("StrictConcurrency")
]

But adding the settings to each target is painful and it’s easy to miss one or forget when adding a new target:

  targets: [
    .target(
        name: "AppFeature",
        dependencies: [
            "AppFeature1",
            "AppFeature2",
            "AppFeature3",
            "AppModel"
        ],
        swiftSettings: settings
    ),
    .testTarget(
        name: "AppFeatureTests",
        dependencies: [
            "AppFeature"
        ],
        swiftSettings: settings
    ),
    .target(...),
    .testTarget(...),
    ...
  ]
)

Applying Settings To All Targets

This Swift forum post gave me a better idea for applying settings to all targets in a package. Adding a few lines of code to the end of the Package.swift file we can iterate over all the package targets appending our Strict Concurrency settings:

for target in package.targets {
  var settings = target.swiftSettings ?? []
  settings.append(.enableExperimentalFeature("StrictConcurrency"))
  target.swiftSettings = settings
}

That covers both regular and test targets. If needed you can identify test targets using the isTest property of the target.

Note: When using Swift 6.0 tools “StrictConcurrency” becomes an upcoming rather than experimental feature:

  settings.append(.enableUpcomingFeature("StrictConcurrency"))

If you’re wondering if a Swift compiler setting is an “experimental” or “upcoming” feature I found this script by Ole Begemann helpful.

Read More