Skip to content

Commit 10bc51d

Browse files
committed
[--build-system swiftbuild] Support xctest discovery and generated test entrypoints
1 parent 789ace3 commit 10bc51d

File tree

7 files changed

+230
-98
lines changed

7 files changed

+230
-98
lines changed

Sources/SPMBuildCore/BuildParameters/BuildParameters.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,26 @@ public struct BuildParameters: Encodable {
314314
case .library(.automatic), .plugin:
315315
fatalError()
316316
case .test:
317-
let base = "\(product.name).xctest"
318-
if self.triple.isDarwin() {
319-
return try RelativePath(validating: "\(base)/Contents/MacOS/\(product.name)")
320-
} else {
321-
return try RelativePath(validating: base)
317+
switch buildSystemKind {
318+
case .native, .xcode:
319+
let base = "\(product.name).xctest"
320+
if self.triple.isDarwin() {
321+
return try RelativePath(validating: "\(base)/Contents/MacOS/\(product.name)")
322+
} else {
323+
return try RelativePath(validating: base)
324+
}
325+
case .swiftbuild:
326+
if self.triple.isDarwin() {
327+
let base = "\(product.name).xctest"
328+
return try RelativePath(validating: "\(base)/Contents/MacOS/\(product.name)")
329+
} else {
330+
var base = "\(product.name)-test-runner"
331+
let ext = self.triple.executableExtension
332+
if !ext.isEmpty {
333+
base += ".\(ext)"
334+
}
335+
return try RelativePath(validating: base)
336+
}
322337
}
323338
case .macro:
324339
#if BUILD_MACROS_AS_DYLIBS

Sources/SPMBuildCore/BuiltTestProduct.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public struct BuiltTestProduct: Codable {
2828
/// When the test product is not bundled (for instance, when using XCTest on
2929
/// non-Darwin targets), this path is equal to ``binaryPath``.
3030
public var bundlePath: AbsolutePath {
31+
// If the binary path is a test runner binary, return it as-is.
32+
guard !binaryPath.basenameWithoutExt.hasSuffix("test-runner") else {
33+
return binaryPath
34+
}
3135
// Go up the folder hierarchy until we find the .xctest bundle.
3236
let pathExtension = ".xctest"
3337
let hierarchySequence = sequence(first: binaryPath, next: { $0.isRoot ? nil : $0.parentDirectory })

Sources/SwiftBuildSupport/PIFBuilder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,13 @@ fileprivate func buildAggregateProject(
369369
continue
370370
}
371371
}
372-
372+
373373
aggregateProject[keyPath: allIncludingTestsTargetKeyPath].common.addDependency(
374374
on: target.id,
375375
platformFilters: [],
376376
linkProduct: false
377377
)
378-
if target.productType != .unitTest {
378+
if ![.unitTest, .swiftpmTestRunner].contains(target.productType) {
379379
aggregateProject[keyPath: allExcludingTestsTargetKeyPath].common.addDependency(
380380
on: target.id,
381381
platformFilters: [],

Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ extension ProjectModel.BuildSettings {
897897
// Appending implies the setting is resilient to having ["$(inherited)"]
898898
self.platformSpecificSettings[platform]![setting]!.append(contentsOf: values)
899899

900-
case .SWIFT_VERSION:
900+
case .SWIFT_VERSION, .DYLIB_INSTALL_NAME_BASE:
901901
self.platformSpecificSettings[platform]![setting] = values // We are not resilient to $(inherited).
902902

903903
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SPECIALIZATION_SDK_OPTIONS:
@@ -919,6 +919,9 @@ extension ProjectModel.BuildSettings {
919919
case .SWIFT_VERSION:
920920
self[.SWIFT_VERSION] = values.only.unwrap(orAssert: "Invalid values for 'SWIFT_VERSION': \(values)")
921921

922+
case .DYLIB_INSTALL_NAME_BASE:
923+
self[.DYLIB_INSTALL_NAME_BASE] = values.only.unwrap(orAssert: "Invalid values for 'DYLIB_INSTALL_NAME_BASE': \(values)")
924+
922925
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SPECIALIZATION_SDK_OPTIONS:
923926
fatalError("Unexpected BuildSettings.Declaration: \(setting)")
924927
}
@@ -947,7 +950,7 @@ extension ProjectModel.BuildSettings.MultipleValueSetting {
947950
self = .SPECIALIZATION_SDK_OPTIONS
948951
case .SWIFT_ACTIVE_COMPILATION_CONDITIONS:
949952
self = .SWIFT_ACTIVE_COMPILATION_CONDITIONS
950-
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SWIFT_VERSION:
953+
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SWIFT_VERSION, .DYLIB_INSTALL_NAME_BASE:
951954
return nil
952955
}
953956
}

Sources/SwiftBuildSupport/PackagePIFBuilder.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ public final class PackagePIFBuilder {
340340
case framework
341341
case executable
342342
case unitTest
343+
case unitTestRunner
343344
case bundle
344345
case resourceBundle
345346
case packageProduct
@@ -363,6 +364,7 @@ public final class PackagePIFBuilder {
363364
case .framework: .framework
364365
case .executable: .executable
365366
case .unitTest: .unitTest
367+
case .swiftpmTestRunner: .unitTestRunner
366368
case .bundle: .bundle
367369
case .packageProduct: .packageProduct
368370
case .hostBuildTool: fatalError("Unexpected hostBuildTool type")
@@ -498,7 +500,15 @@ public final class PackagePIFBuilder {
498500
settings[.WATCHOS_DEPLOYMENT_TARGET] = builder.deploymentTargets[.watchOS] ?? nil
499501
settings[.DRIVERKIT_DEPLOYMENT_TARGET] = builder.deploymentTargets[.driverKit] ?? nil
500502
settings[.XROS_DEPLOYMENT_TARGET] = builder.deploymentTargets[.visionOS] ?? nil
501-
settings[.DYLIB_INSTALL_NAME_BASE] = "@rpath"
503+
504+
for machoPlatform in [ProjectModel.BuildSettings.Platform.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .xrOS, .driverKit] {
505+
settings.platformSpecificSettings[machoPlatform]![.DYLIB_INSTALL_NAME_BASE]! = ["@rpath"]
506+
}
507+
for elfPlatform in [ProjectModel.BuildSettings.Platform.linux, .android, .openbsd, .freebsd] {
508+
// FIXME: Treating $ORIGIN as part of the soname is not really the right thing to do here
509+
settings.platformSpecificSettings[elfPlatform]![.DYLIB_INSTALL_NAME_BASE]! = ["$ORIGIN"]
510+
}
511+
502512
settings[.USE_HEADERMAP] = "NO"
503513
settings[.OTHER_SWIFT_FLAGS].lazilyInitializeAndMutate(initialValue: ["$(inherited)"]) { $0.append("-DXcode") }
504514

Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import TSCBasic
1415
import TSCUtility
1516

1617
import struct Basics.AbsolutePath
@@ -128,6 +129,8 @@ extension PackagePIFProjectBuilder {
128129
settings[.GENERATE_INFOPLIST_FILE] = "YES"
129130
settings[.SKIP_INSTALL] = "NO"
130131
settings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS].lazilyInitialize { ["$(inherited)"] }
132+
// Enable index-while building for Swift compilations to facilitate discovery of XCTest tests.
133+
settings[.SWIFT_INDEX_STORE_ENABLE] = "YES"
131134
} else if mainModule.type == .executable {
132135
// Setup install path for executables if it's in root of a pure Swift package.
133136
if pifBuilder.delegate.hostsOnlyPackages && pifBuilder.delegate.isRootPackage {
@@ -502,9 +505,13 @@ extension PackagePIFProjectBuilder {
502505
linkedPackageBinaries: linkedPackageBinaries,
503506
swiftLanguageVersion: mainModule.packageSwiftLanguageVersion(manifest: packageManifest),
504507
declaredPlatforms: self.declaredPlatforms,
505-
deploymentTargets: self.deploymentTargets
508+
deploymentTargets: mainTargetDeploymentTargets
506509
)
507510
self.builtModulesAndProducts.append(moduleOrProduct)
511+
512+
if moduleOrProductType == .unitTest {
513+
try makeTestRunnerProduct(for: moduleOrProduct)
514+
}
508515
}
509516

510517
private mutating func handleProduct(
@@ -995,6 +1002,102 @@ extension PackagePIFProjectBuilder {
9951002
)
9961003
self.builtModulesAndProducts.append(pluginProductMetadata)
9971004
}
1005+
1006+
// MARK: - Test Runners
1007+
mutating func makeTestRunnerProduct(for unitTestProduct: PackagePIFBuilder.ModuleOrProduct) throws {
1008+
// Only generate a test runner for root packages with tests.
1009+
guard pifBuilder.delegate.isRootPackage else {
1010+
return
1011+
}
1012+
1013+
guard let unitTestModuleName = unitTestProduct.moduleName else {
1014+
throw StringError("Unit test product '\(unitTestProduct.name)' is missing a module name")
1015+
}
1016+
1017+
let name = "\(unitTestProduct.name)-test-runner"
1018+
let moduleName = "\(unitTestModuleName)_test_runner"
1019+
let guid = PackagePIFBuilder.targetGUID(forModuleName: moduleName)
1020+
1021+
let testRunnerTargetKeyPath = try self.project.addTarget { _ in
1022+
ProjectModel.Target (
1023+
id: guid,
1024+
productType: .swiftpmTestRunner,
1025+
name: name,
1026+
productName: name
1027+
)
1028+
}
1029+
1030+
var settings: BuildSettings = self.package.underlying.packageBaseBuildSettings
1031+
let impartedSettings = BuildSettings()
1032+
1033+
settings[.TARGET_NAME] = name
1034+
settings[.PACKAGE_RESOURCE_TARGET_KIND] = "regular"
1035+
settings[.PRODUCT_NAME] = "$(TARGET_NAME)"
1036+
settings[.PRODUCT_MODULE_NAME] = moduleName
1037+
settings[.PRODUCT_BUNDLE_IDENTIFIER] = "\(self.package.identity).\(name)"
1038+
.spm_mangledToBundleIdentifier()
1039+
settings[.EXECUTABLE_NAME] = name
1040+
settings[.SKIP_INSTALL] = "NO"
1041+
settings[.SWIFT_VERSION] = "5.0"
1042+
// This should eventually be set universally for all package targets/products.
1043+
settings[.LINKER_DRIVER] = "swiftc"
1044+
1045+
let deploymentTargets = unitTestProduct.deploymentTargets
1046+
settings[.MACOSX_DEPLOYMENT_TARGET] = deploymentTargets?[.macOS] ?? nil
1047+
settings[.IPHONEOS_DEPLOYMENT_TARGET] = deploymentTargets?[.iOS] ?? nil
1048+
if let deploymentTarget_macCatalyst = deploymentTargets?[.macCatalyst] ?? nil {
1049+
settings.platformSpecificSettings[.macCatalyst]![.IPHONEOS_DEPLOYMENT_TARGET] = [deploymentTarget_macCatalyst]
1050+
}
1051+
settings[.TVOS_DEPLOYMENT_TARGET] = deploymentTargets?[.tvOS] ?? nil
1052+
settings[.WATCHOS_DEPLOYMENT_TARGET] = deploymentTargets?[.watchOS] ?? nil
1053+
settings[.DRIVERKIT_DEPLOYMENT_TARGET] = deploymentTargets?[.driverKit] ?? nil
1054+
settings[.XROS_DEPLOYMENT_TARGET] = deploymentTargets?[.visionOS] ?? nil
1055+
1056+
// Add an empty sources phase so derived sources are compiled
1057+
self.project[keyPath: testRunnerTargetKeyPath].common.addSourcesBuildPhase { id in
1058+
ProjectModel.SourcesBuildPhase(id: id)
1059+
}
1060+
1061+
guard let unitTestGUID = unitTestProduct.pifTarget?.id else {
1062+
throw StringError("Unit test product '\(unitTestProduct.name)' is missing a PIF GUID")
1063+
}
1064+
self.project[keyPath: testRunnerTargetKeyPath].common.addDependency(
1065+
on: unitTestGUID,
1066+
platformFilters: [],
1067+
linkProduct: true
1068+
)
1069+
1070+
self.project[keyPath: testRunnerTargetKeyPath].common.addBuildConfig { id in
1071+
BuildConfig(
1072+
id: id,
1073+
name: "Debug",
1074+
settings: settings,
1075+
impartedBuildSettings: impartedSettings
1076+
)
1077+
}
1078+
self.project[keyPath: testRunnerTargetKeyPath].common.addBuildConfig { id in
1079+
BuildConfig(
1080+
id: id,
1081+
name: "Release",
1082+
settings: settings,
1083+
impartedBuildSettings: impartedSettings
1084+
)
1085+
}
1086+
1087+
let testRunner = PackagePIFBuilder.ModuleOrProduct(
1088+
type: .unitTestRunner,
1089+
name: name,
1090+
moduleName: moduleName,
1091+
pifTarget: .target(self.project[keyPath: testRunnerTargetKeyPath]),
1092+
indexableFileURLs: [],
1093+
headerFiles: [],
1094+
linkedPackageBinaries: [],
1095+
swiftLanguageVersion: nil,
1096+
declaredPlatforms: self.declaredPlatforms,
1097+
deploymentTargets: self.deploymentTargets
1098+
)
1099+
self.builtModulesAndProducts.append(testRunner)
1100+
}
9981101
}
9991102

10001103
// MARK: - Helper Types

0 commit comments

Comments
 (0)