Skip to content

Commit 3db835d

Browse files
authored
Non-darwin test discovery with Swift Build (#8722)
Still needs a fair amount of work and cleanup, I'll look at breaking off some parts that can land independently. Depends on the larger patch in swiftlang/swift-build#499
1 parent 065df19 commit 3db835d

File tree

10 files changed

+280
-124
lines changed

10 files changed

+280
-124
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
@@ -633,13 +633,13 @@ fileprivate func buildAggregateProject(
633633
continue
634634
}
635635
}
636-
636+
637637
aggregateProject[keyPath: allIncludingTestsTargetKeyPath].common.addDependency(
638638
on: target.id,
639639
platformFilters: [],
640640
linkProduct: false
641641
)
642-
if target.productType != .unitTest {
642+
if ![.unitTest, .swiftpmTestRunner].contains(target.productType) {
643643
aggregateProject[keyPath: allExcludingTestsTargetKeyPath].common.addDependency(
644644
on: target.id,
645645
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:
@@ -922,6 +922,9 @@ extension ProjectModel.BuildSettings {
922922
case .SWIFT_VERSION:
923923
self[.SWIFT_VERSION] = values.only.unwrap(orAssert: "Invalid values for 'SWIFT_VERSION': \(values)")
924924

925+
case .DYLIB_INSTALL_NAME_BASE:
926+
self[.DYLIB_INSTALL_NAME_BASE] = values.only.unwrap(orAssert: "Invalid values for 'DYLIB_INSTALL_NAME_BASE': \(values)")
927+
925928
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SPECIALIZATION_SDK_OPTIONS:
926929
fatalError("Unexpected BuildSettings.Declaration: \(setting)")
927930
// Allow staging in new cases
@@ -953,7 +956,7 @@ extension ProjectModel.BuildSettings.MultipleValueSetting {
953956
self = .SPECIALIZATION_SDK_OPTIONS
954957
case .SWIFT_ACTIVE_COMPILATION_CONDITIONS:
955958
self = .SWIFT_ACTIVE_COMPILATION_CONDITIONS
956-
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SWIFT_VERSION:
959+
case .ARCHS, .IPHONEOS_DEPLOYMENT_TARGET, .SWIFT_VERSION, .DYLIB_INSTALL_NAME_BASE:
957960
return nil
958961
// Allow staging in new cases
959962
default:

Sources/SwiftBuildSupport/PackagePIFBuilder.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ public final class PackagePIFBuilder {
363363
case framework
364364
case executable
365365
case unitTest
366+
case unitTestRunner
366367
case bundle
367368
case resourceBundle
368369
case packageProduct
@@ -386,6 +387,7 @@ public final class PackagePIFBuilder {
386387
case .framework: .framework
387388
case .executable: .executable
388389
case .unitTest: .unitTest
390+
case .swiftpmTestRunner: .unitTestRunner
389391
case .bundle: .bundle
390392
case .packageProduct: .packageProduct
391393
case .hostBuildTool: fatalError("Unexpected hostBuildTool type")
@@ -521,7 +523,11 @@ public final class PackagePIFBuilder {
521523
settings[.WATCHOS_DEPLOYMENT_TARGET] = builder.deploymentTargets[.watchOS] ?? nil
522524
settings[.DRIVERKIT_DEPLOYMENT_TARGET] = builder.deploymentTargets[.driverKit] ?? nil
523525
settings[.XROS_DEPLOYMENT_TARGET] = builder.deploymentTargets[.visionOS] ?? nil
524-
settings[.DYLIB_INSTALL_NAME_BASE] = "@rpath"
526+
527+
for machoPlatform in [ProjectModel.BuildSettings.Platform.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .xrOS, .driverKit] {
528+
settings.platformSpecificSettings[machoPlatform]![.DYLIB_INSTALL_NAME_BASE]! = ["@rpath"]
529+
}
530+
525531
settings[.USE_HEADERMAP] = "NO"
526532
settings[.OTHER_SWIFT_FLAGS].lazilyInitializeAndMutate(initialValue: ["$(inherited)"]) { $0.append("-DXcode") }
527533

Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ extension PackagePIFProjectBuilder {
768768
//
769769
// An imparted build setting on C will propagate back to both B and A.
770770
impartedSettings[.LD_RUNPATH_SEARCH_PATHS] =
771-
["@loader_path"] +
771+
["$(RPATH_ORIGIN)"] +
772772
(impartedSettings[.LD_RUNPATH_SEARCH_PATHS] ?? ["$(inherited)"])
773773

774774
var impartedDebugSettings = impartedSettings

Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift

Lines changed: 106 additions & 3 deletions
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
@@ -121,13 +122,15 @@ extension PackagePIFProjectBuilder {
121122
if mainModule.type == .test {
122123
// FIXME: we shouldn't always include both the deep and shallow bundle paths here, but for that we'll need rdar://31867023
123124
settings[.LD_RUNPATH_SEARCH_PATHS] = [
124-
"@loader_path/Frameworks",
125-
"@loader_path/../Frameworks",
125+
"$(RPATH_ORIGIN)/Frameworks",
126+
"$(RPATH_ORIGIN)/../Frameworks",
126127
"$(inherited)"
127128
]
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

Tests/CommandsTests/PackageCommandTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4107,7 +4107,6 @@ class PackageCommandSwiftBuildTests: PackageCommandTestCase {
41074107
}
41084108

41094109
override func testCommandPluginTestingCallbacks() async throws {
4110-
throw XCTSkip("SWBINTTODO: Requires PIF generation to adopt new test runner product type")
41114110
try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602")
41124111
try await super.testCommandPluginTestingCallbacks()
41134112
}

0 commit comments

Comments
 (0)