Skip to content

Commit 0d6fa53

Browse files
committed
WIP: Implement SE-0455: SwiftPM @testable build setting
This introduces a new `enableTesting` API in SwiftSetting which allows targets to explicitly control whether testability is enabled, as some projects do not want to use the @testable feature. This can also improve debug build performance as significantly fewer symbols will be exported from the binary in the case where @testable is disabled. The default is equivalent to `enableTesting(true, .when(configuration: .debug))`, so there is no behavior change from today without explicitly adopting this new API. The --enable-testable-imports/--disable-testable-imports command line flags now acts as overrides -- if specified, they will override any build settings configured at the target level, and if unspecified, the target-level settings will be respected.
1 parent 4878aba commit 0d6fa53

File tree

11 files changed

+76
-27
lines changed

11 files changed

+76
-27
lines changed

Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,8 @@ public final class SwiftModuleBuildDescription {
979979

980980
/// Testing arguments according to the build configuration.
981981
private var testingArguments: [String] {
982+
let enableTesting: Bool
983+
982984
if self.isTestTarget {
983985
// test targets must be built with -enable-testing
984986
// since its required for test discovery (the non objective-c reflection kind)
@@ -991,11 +993,27 @@ public final class SwiftModuleBuildDescription {
991993
result += ["-Xfrontend", "-enable-cross-import-overlays"]
992994

993995
return result
994-
} else if self.buildParameters.enableTestability {
995-
return ["-enable-testing"]
996+
} else if let enableTestability = self.buildParameters.testingParameters.explicitlyEnabledTestability {
997+
// Let the command line flag override
998+
enableTesting = enableTestability
996999
} else {
997-
return []
1000+
// Use the target settings
1001+
let enableTestabilitySetting = self.buildParameters.createScope(for: self.target).evaluate(.ENABLE_TESTABILITY)
1002+
if !enableTestabilitySetting.isEmpty {
1003+
enableTesting = enableTestabilitySetting.contains(where: { $0 == "YES" })
1004+
} else {
1005+
// By default, decide on testability based on debug/release config
1006+
// the goals of this being based on the build configuration is
1007+
// that `swift build` followed by a `swift test` will need to do minimal rebuilding
1008+
// given that the default configuration for `swift build` is debug
1009+
// and that `swift test` requires building with testable enabled if @testable is being used.
1010+
// when building and testing in release mode, one can use the '--disable-testable-imports' flag
1011+
// to disable testability in `swift test`, but that requires that the tests do not use the @testable imports feature
1012+
enableTesting = self.buildParameters.configuration == .debug
1013+
}
9981014
}
1015+
1016+
return enableTesting ? ["-enable-testing"] : []
9991017
}
10001018

10011019
/// Module cache arguments.

Sources/Commands/SwiftTestCommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ struct TestCommandOptions: ParsableArguments {
189189
var xUnitOutput: AbsolutePath?
190190

191191
/// Generate LinuxMain entries and exit.
192-
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Enabled by default.")
193-
var enableTestableImports: Bool = true
192+
@Flag(name: .customLong("testable-imports"), inversion: .prefixedEnableDisable, help: "Enable or disable testable imports. Based on target settings by default.")
193+
var enableTestableImports: Bool?
194194

195195
/// Whether to enable code coverage.
196196
@Flag(name: .customLong("code-coverage"),

Sources/PackageDescription/BuildSettings.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,22 @@ public struct SwiftSetting: Sendable {
440440
return SwiftSetting(
441441
name: "swiftLanguageMode", value: [.init(describing: mode)], condition: condition)
442442
}
443+
444+
/// Whether `@testable` is enabled by passing the `-enable-testing` to the Swift compiler.
445+
///
446+
/// - Since: First available in PackageDescription 6.2.
447+
///
448+
/// - Parameters:
449+
/// - enable: Whether to enable `@testable`.
450+
/// - condition: A condition that restricts the application of the build setting.
451+
@available(_PackageDescription, introduced: 6.2)
452+
public static func enableTestableImport(
453+
_ enable: Bool,
454+
_ condition: BuildSettingCondition? = nil
455+
) -> SwiftSetting {
456+
return SwiftSetting(
457+
name: "enableTestableImport", value: [.init(describing: enable)], condition: condition)
458+
}
443459
}
444460

445461
/// A linker build setting.

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,20 @@ extension TargetBuildSettingDescription.Kind {
554554
}
555555

556556
return .swiftLanguageMode(version)
557+
case "enableTestableImport":
558+
guard let rawVersion = values.first else {
559+
throw InternalError("invalid (empty) build settings value")
560+
}
561+
562+
if values.count > 1 {
563+
throw InternalError("invalid build settings value")
564+
}
565+
566+
guard let value = Bool(rawVersion) else {
567+
throw InternalError("invalid boolean value: \(rawVersion)")
568+
}
569+
570+
return .enableTestableImport(value)
557571
default:
558572
throw InternalError("invalid build setting \(name)")
559573
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,12 @@ public final class PackageBuilder {
10161016
}
10171017
}
10181018

1019+
for setting in manifestTarget.settings {
1020+
if case let .enableTestableImport(enable) = setting.kind, enable, setting.condition?.config == "release" {
1021+
self.observabilityScope.emit(warning: "'\(potentialModule.name)' should not enable `@testable import` when building in release mode")
1022+
}
1023+
}
1024+
10191025
// Create and return the right kind of target depending on what kind of sources we found.
10201026
if sources.hasSwiftSources {
10211027
return try SwiftModule(
@@ -1223,6 +1229,10 @@ public final class PackageBuilder {
12231229
}
12241230

12251231
values = [version.rawValue]
1232+
1233+
case .enableTestableImport(let enable):
1234+
decl = .ENABLE_TESTABILITY
1235+
values = enable ? ["YES"] : ["NO"]
12261236
}
12271237

12281238
// Create an assignment for this setting.

Sources/PackageModel/BuildSettings.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
public enum BuildSettings {
1515
/// Build settings declarations.
1616
public struct Declaration: Hashable {
17+
public static let ENABLE_TESTABILITY: Declaration = .init("ENABLE_TESTABILITY")
18+
1719
// Swift.
1820
public static let SWIFT_ACTIVE_COMPILATION_CONDITIONS: Declaration =
1921
.init("SWIFT_ACTIVE_COMPILATION_CONDITIONS")

Sources/PackageModel/Manifest/TargetBuildSettingDescription.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ public enum TargetBuildSettingDescription {
4141

4242
case swiftLanguageMode(SwiftLanguageVersion)
4343

44+
case enableTestableImport(Bool)
45+
4446
public var isUnsafeFlags: Bool {
4547
switch self {
4648
case .unsafeFlags(let flags):
4749
// If `.unsafeFlags` is used, but doesn't specify any flags, we treat it the same way as not specifying it.
4850
return !flags.isEmpty
4951
case .headerSearchPath, .define, .linkedLibrary, .linkedFramework, .interoperabilityMode,
50-
.enableUpcomingFeature, .enableExperimentalFeature, .swiftLanguageMode:
52+
.enableUpcomingFeature, .enableExperimentalFeature, .swiftLanguageMode, .enableTestableImport:
5153
return false
5254
}
5355
}

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,12 @@ fileprivate extension SourceCodeFragment {
534534
params.append(SourceCodeFragment(from: condition))
535535
}
536536
self.init(enum: setting.kind.name, subnodes: params)
537+
case .enableTestableImport(let enable):
538+
params.append(SourceCodeFragment(boolean: enable))
539+
if let condition = setting.condition {
540+
params.append(SourceCodeFragment(from: condition))
541+
}
542+
self.init(enum: setting.kind.name, subnodes: params)
537543
}
538544
}
539545
}
@@ -688,6 +694,8 @@ extension TargetBuildSettingDescription.Kind {
688694
return "enableExperimentalFeature"
689695
case .swiftLanguageMode:
690696
return "swiftLanguageMode"
697+
case .enableTestableImport:
698+
return "enableTestableImport"
691699
}
692700
}
693701
}

Sources/SPMBuildCore/BuildParameters/BuildParameters+Testing.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,6 @@ extension BuildParameters {
105105
}
106106
}
107107

108-
/// Whether building for testability is enabled.
109-
public var enableTestability: Bool {
110-
// decide on testability based on debug/release config
111-
// the goals of this being based on the build configuration is
112-
// that `swift build` followed by a `swift test` will need to do minimal rebuilding
113-
// given that the default configuration for `swift build` is debug
114-
// and that `swift test` normally requires building with testable enabled.
115-
// when building and testing in release mode, one can use the '--disable-testable-imports' flag
116-
// to disable testability in `swift test`, but that requires that the tests do not use the testable imports feature
117-
self.testingParameters.explicitlyEnabledTestability ?? (self.configuration == .debug)
118-
}
119-
120108
/// The style of test product to produce.
121109
public var testProductStyle: TestProductStyle {
122110
return triple.isDarwin() ? .loadableBundle : .entryPointExecutable(

Sources/XCBuildSupport/PIFBuilder.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ struct PIFBuilderParameters {
2929
/// Whether the toolchain supports `-package-name` option.
3030
let isPackageAccessModifierSupported: Bool
3131

32-
/// Whether or not build for testability is enabled.
33-
let enableTestability: Bool
34-
3532
/// Whether to create dylibs for dynamic library products.
3633
let shouldCreateDylibForDynamicProducts: Bool
3734

@@ -343,7 +340,6 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
343340
debugSettings[.GCC_OPTIMIZATION_LEVEL] = "0"
344341
debugSettings[.ONLY_ACTIVE_ARCH] = "YES"
345342
debugSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Onone"
346-
debugSettings[.ENABLE_TESTABILITY] = "YES"
347343
debugSettings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS, default: []].append("DEBUG")
348344
debugSettings[.GCC_PREPROCESSOR_DEFINITIONS, default: ["$(inherited)"]].append("DEBUG=1")
349345
addBuildConfiguration(name: "Debug", settings: debugSettings)
@@ -354,10 +350,6 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder {
354350
releaseSettings[.GCC_OPTIMIZATION_LEVEL] = "s"
355351
releaseSettings[.SWIFT_OPTIMIZATION_LEVEL] = "-Owholemodule"
356352

357-
if parameters.enableTestability {
358-
releaseSettings[.ENABLE_TESTABILITY] = "YES"
359-
}
360-
361353
addBuildConfiguration(name: "Release", settings: releaseSettings)
362354

363355
for product in package.products.sorted(by: { $0.name < $1.name }) {

Sources/XCBuildSupport/XcodeBuildSystem.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,6 @@ extension PIFBuilderParameters {
371371
self.init(
372372
triple: buildParameters.triple,
373373
isPackageAccessModifierSupported: buildParameters.driverParameters.isPackageAccessModifierSupported,
374-
enableTestability: buildParameters.enableTestability,
375374
shouldCreateDylibForDynamicProducts: buildParameters.shouldCreateDylibForDynamicProducts,
376375
toolchainLibDir: (try? buildParameters.toolchain.toolchainLibDir) ?? .root,
377376
pkgConfigDirectories: buildParameters.pkgConfigDirectories,

0 commit comments

Comments
 (0)