diff --git a/Sources/Commands/Utilities/TestingSupport.swift b/Sources/Commands/Utilities/TestingSupport.swift index 1291ac73e43..8e21027a783 100644 --- a/Sources/Commands/Utilities/TestingSupport.swift +++ b/Sources/Commands/Utilities/TestingSupport.swift @@ -176,8 +176,11 @@ enum TestingSupport { } #if !os(macOS) #if os(Windows) - if let location = toolchain.xctestPath { - env.prependPath(key: .path, value: location.pathString) + if let xctestLocation = toolchain.xctestPath { + env.prependPath(key: .path, value: xctestLocation.pathString) + } + if let swiftTestingLocation = toolchain.swiftTestingPathOnWindows { + env.prependPath(key: .path, value: swiftTestingLocation.pathString) } #endif return env diff --git a/Sources/PackageModel/Toolchain.swift b/Sources/PackageModel/Toolchain.swift index e5097aa0460..fc710f11170 100644 --- a/Sources/PackageModel/Toolchain.swift +++ b/Sources/PackageModel/Toolchain.swift @@ -71,20 +71,24 @@ extension Toolchain { public var hostLibDir: AbsolutePath { get throws { - return try toolchainLibDir.appending(components: ["swift", "host"]) + try Self.toolchainLibDir(swiftCompilerPath: self.swiftCompilerPath).appending( + components: ["swift", "host"] + ) } } public var macosSwiftStdlib: AbsolutePath { get throws { - return try AbsolutePath(validating: "../../lib/swift/macosx", relativeTo: resolveSymlinks(swiftCompilerPath)) + try Self.toolchainLibDir(swiftCompilerPath: self.swiftCompilerPath).appending( + components: ["swift", "macosx"] + ) } } public var toolchainLibDir: AbsolutePath { get throws { // FIXME: Not sure if it's better to base this off of Swift compiler or our own binary. - return try AbsolutePath(validating: "../../lib", relativeTo: resolveSymlinks(swiftCompilerPath)) + try Self.toolchainLibDir(swiftCompilerPath: self.swiftCompilerPath) } } @@ -107,4 +111,8 @@ extension Toolchain { public var extraSwiftCFlags: [String] { extraFlags.swiftCompilerFlags } + + package static func toolchainLibDir(swiftCompilerPath: AbsolutePath) throws -> AbsolutePath { + try AbsolutePath(validating: "../../lib", relativeTo: resolveSymlinks(swiftCompilerPath)) + } } diff --git a/Sources/PackageModel/ToolchainConfiguration.swift b/Sources/PackageModel/ToolchainConfiguration.swift index 7f9da9746c5..79d49a1aa04 100644 --- a/Sources/PackageModel/ToolchainConfiguration.swift +++ b/Sources/PackageModel/ToolchainConfiguration.swift @@ -42,6 +42,10 @@ public struct ToolchainConfiguration { /// This is optional for example on macOS w/o Xcode. public var xctestPath: AbsolutePath? + /// Path to the swift-testing utility. + /// Currently computed only for Windows. + public var swiftTestingPath: AbsolutePath? + /// Creates the set of manifest resources associated with a `swiftc` executable. /// /// - Parameters: @@ -59,7 +63,8 @@ public struct ToolchainConfiguration { swiftCompilerEnvironment: Environment = .current, swiftPMLibrariesLocation: SwiftPMLibrariesLocation? = nil, sdkRootPath: AbsolutePath? = nil, - xctestPath: AbsolutePath? = nil + xctestPath: AbsolutePath? = nil, + swiftTestingPath: AbsolutePath? = nil ) { let swiftPMLibrariesLocation = swiftPMLibrariesLocation ?? { return .init(swiftCompilerPath: swiftCompilerPath) @@ -72,6 +77,7 @@ public struct ToolchainConfiguration { self.swiftPMLibrariesLocation = swiftPMLibrariesLocation self.sdkRootPath = sdkRootPath self.xctestPath = xctestPath + self.swiftTestingPath = swiftTestingPath } } diff --git a/Sources/PackageModel/UserToolchain.swift b/Sources/PackageModel/UserToolchain.swift index ed4a1299437..60348d4f7da 100644 --- a/Sources/PackageModel/UserToolchain.swift +++ b/Sources/PackageModel/UserToolchain.swift @@ -403,6 +403,40 @@ public final class UserToolchain: Toolchain { } #endif + /// On MacOS toolchain can shadow SDK content. This method is intended + /// to locate and include swift-testing library from a toolchain before + /// sdk content which to sure that builds that use a custom toolchain + /// always get a custom swift-testing library as well. + static func deriveMacOSSpecificSwiftTestingFlags( + derivedSwiftCompiler: AbsolutePath, + fileSystem: any FileSystem + ) -> [String] { + guard let toolchainLibDir = try? toolchainLibDir( + swiftCompilerPath: derivedSwiftCompiler + ) else { + return [] + } + + let testingLibDir = toolchainLibDir.appending( + components: ["swift", "macosx", "testing"] + ) + + let testingPluginsDir = toolchainLibDir.appending( + components: ["swift", "host", "plugins", "testing"] + ) + + guard fileSystem.exists(testingLibDir), fileSystem.exists(testingPluginsDir) else { + return [] + } + + return [ + "-I", testingLibDir.pathString, + "-L", testingLibDir.pathString, + "-plugin-path", testingPluginsDir.pathString, + "-Xlinker", "-rpath", "-Xlinker", testingLibDir.pathString, + ] + } + internal static func deriveSwiftCFlags( triple: Triple, swiftSDK: SwiftSDK, @@ -420,9 +454,10 @@ public final class UserToolchain: Toolchain { // Windows uses a variable named SDKROOT to determine the root of // the SDK. This is not the same value as the SDKROOT parameter // in Xcode, however, the value represents a similar concept. - if let SDKROOT = environment["SDKROOT"], let sdkroot = try? AbsolutePath(validating: SDKROOT) { + if let sdkroot = environment.windowsSDKRoot { var runtime: [String] = [] var xctest: [String] = [] + var swiftTesting: [String] = [] var extraSwiftCFlags: [String] = [] if let settings = WindowsSDKSettings( @@ -444,7 +479,7 @@ public final class UserToolchain: Toolchain { // The layout of the SDK is as follows: // - // Library/Developer/Platforms/[PLATFORM].platform/Developer/Library/XCTest-[VERSION]/... + // Library/Developer/Platforms/[PLATFORM].platform/Developer/Library/-[VERSION]/... // Library/Developer/Platforms/[PLATFORM].platform/Developer/SDKs/[PLATFORM].sdk/... // // SDKROOT points to [PLATFORM].sdk @@ -455,14 +490,17 @@ public final class UserToolchain: Toolchain { observabilityScope: nil, filesystem: fileSystem ) { - let installation: AbsolutePath = + let XCTestInstallation: AbsolutePath = platform.appending("Developer") .appending("Library") .appending("XCTest-\(info.defaults.xctestVersion)") xctest = try [ "-I", - AbsolutePath(validating: "usr/lib/swift/windows", relativeTo: installation).pathString, + AbsolutePath( + validating: "usr/lib/swift/windows", + relativeTo: XCTestInstallation + ).pathString, // Migration Path // // Older Swift (<=5.7) installations placed the @@ -475,11 +513,13 @@ public final class UserToolchain: Toolchain { "-I", AbsolutePath( validating: "usr/lib/swift/windows/\(triple.archName)", - relativeTo: installation + relativeTo: XCTestInstallation ).pathString, "-L", - AbsolutePath(validating: "usr/lib/swift/windows/\(triple.archName)", relativeTo: installation) - .pathString, + AbsolutePath( + validating: "usr/lib/swift/windows/\(triple.archName)", + relativeTo: XCTestInstallation + ).pathString, ] // Migration Path @@ -492,16 +532,36 @@ public final class UserToolchain: Toolchain { // architecture subdirectory in `bin` if available. let implib = try AbsolutePath( validating: "usr/lib/swift/windows/XCTest.lib", - relativeTo: installation + relativeTo: XCTestInstallation ) if fileSystem.exists(implib) { xctest.append(contentsOf: ["-L", implib.parentDirectory.pathString]) } + if let swiftTestingVersion = info.defaults.swiftTestingVersion { + let swiftTestingInstallation: AbsolutePath = + platform.appending("Developer") + .appending("Library") + .appending("Testing-\(swiftTestingVersion)") + + swiftTesting = try [ + "-I", + AbsolutePath( + validating: "usr/lib/swift/windows", + relativeTo: swiftTestingInstallation + ).pathString, + "-L", + AbsolutePath( + validating: "usr/lib/swift/windows/\(triple.archName)", + relativeTo: swiftTestingInstallation + ).pathString + ] + } + extraSwiftCFlags = info.defaults.extraSwiftCFlags ?? [] } - return ["-sdk", sdkroot.pathString] + runtime + xctest + extraSwiftCFlags + return ["-sdk", sdkroot.pathString] + runtime + xctest + swiftTesting + extraSwiftCFlags } } @@ -596,15 +656,25 @@ public final class UserToolchain: Toolchain { self.targetTriple = triple + var swiftCompilerFlags: [String] = [] + #if os(macOS) + swiftCompilerFlags += Self.deriveMacOSSpecificSwiftTestingFlags( + derivedSwiftCompiler: swiftCompilers.compile, + fileSystem: fileSystem + ) + #endif + + swiftCompilerFlags += try Self.deriveSwiftCFlags( + triple: triple, + swiftSDK: swiftSDK, + environment: environment, + fileSystem: fileSystem + ) + self.extraFlags = BuildFlags( cCompilerFlags: swiftSDK.toolset.knownTools[.cCompiler]?.extraCLIOptions ?? [], cxxCompilerFlags: swiftSDK.toolset.knownTools[.cxxCompiler]?.extraCLIOptions ?? [], - swiftCompilerFlags: try Self.deriveSwiftCFlags( - triple: triple, - swiftSDK: swiftSDK, - environment: environment, - fileSystem: fileSystem - ), + swiftCompilerFlags: swiftCompilerFlags, linkerFlags: swiftSDK.toolset.knownTools[.linker]?.extraCLIOptions ?? [], xcbuildFlags: swiftSDK.toolset.knownTools[.xcbuild]?.extraCLIOptions ?? []) @@ -627,7 +697,7 @@ public final class UserToolchain: Toolchain { } if triple.isWindows() { - if let SDKROOT = environment["SDKROOT"], let root = try? AbsolutePath(validating: SDKROOT) { + if let root = environment.windowsSDKRoot { if let settings = WindowsSDKSettings( reading: root.appending("SDKSettings.plist"), observabilityScope: nil, @@ -683,6 +753,19 @@ public final class UserToolchain: Toolchain { ) } + let swiftTestingPath: AbsolutePath? + if case .custom(_, let useXcrun) = searchStrategy, !useXcrun { + swiftTestingPath = nil + } else { + swiftTestingPath = try Self.deriveSwiftTestingPath( + swiftSDK: self.swiftSDK, + triple: triple, + environment: environment, + fileSystem: fileSystem + ) + } + + self.configuration = .init( librarianPath: librarianPath, swiftCompilerPath: swiftCompilers.manifest, @@ -690,7 +773,8 @@ public final class UserToolchain: Toolchain { swiftCompilerEnvironment: environment, swiftPMLibrariesLocation: swiftPMLibrariesLocation, sdkRootPath: self.swiftSDK.pathsConfiguration.sdkRootPath, - xctestPath: xctestPath + xctestPath: xctestPath, + swiftTestingPath: swiftTestingPath ) self.fileSystem = fileSystem @@ -786,6 +870,42 @@ public final class UserToolchain: Toolchain { return .none } + private static func getWindowsPlatformInfo( + swiftSDK: SwiftSDK, + environment: Environment, + fileSystem: any FileSystem + ) -> (AbsolutePath, WindowsPlatformInfo)? { + let sdkRoot: AbsolutePath? = if let sdkDir = swiftSDK.pathsConfiguration.sdkRootPath { + sdkDir + } else if let sdkDir = environment.windowsSDKRoot { + sdkDir + } else { + nil + } + + guard let sdkRoot else { + return nil + } + + // The layout of the SDK is as follows: + // + // Library/Developer/Platforms/[PLATFORM].platform/Developer/Library/-[VERSION]/... + // Library/Developer/Platforms/[PLATFORM].platform/Developer/SDKs/[PLATFORM].sdk/... + // + // SDKROOT points to [PLATFORM].sdk + let platform = sdkRoot.parentDirectory.parentDirectory.parentDirectory + + guard let info = WindowsPlatformInfo( + reading: platform.appending("Info.plist"), + observabilityScope: nil, + filesystem: fileSystem + ) else { + return nil + } + + return (platform, info) + } + // TODO: We should have some general utility to find tools. private static func deriveXCTestPath( swiftSDK: SwiftSDK, @@ -802,28 +922,10 @@ public final class UserToolchain: Toolchain { return try AbsolutePath(validating: path) } } else if triple.isWindows() { - let sdkRoot: AbsolutePath - - if let sdkDir = swiftSDK.pathsConfiguration.sdkRootPath { - sdkRoot = sdkDir - } else if let SDKROOT = environment["SDKROOT"], let sdkDir = try? AbsolutePath(validating: SDKROOT) { - sdkRoot = sdkDir - } else { - return .none - } - - // The layout of the SDK is as follows: - // - // Library/Developer/Platforms/[PLATFORM].platform/Developer/Library/XCTest-[VERSION]/... - // Library/Developer/Platforms/[PLATFORM].platform/Developer/SDKs/[PLATFORM].sdk/... - // - // SDKROOT points to [PLATFORM].sdk - let platform = sdkRoot.parentDirectory.parentDirectory.parentDirectory - - if let info = WindowsPlatformInfo( - reading: platform.appending("Info.plist"), - observabilityScope: nil, - filesystem: fileSystem + if let (platform, info) = getWindowsPlatformInfo( + swiftSDK: swiftSDK, + environment: environment, + fileSystem: fileSystem ) { let xctest: AbsolutePath = platform.appending("Developer") @@ -886,6 +988,57 @@ public final class UserToolchain: Toolchain { return .none } + private static func deriveSwiftTestingPath( + swiftSDK: SwiftSDK, + triple: Triple, + environment: Environment, + fileSystem: any FileSystem + ) throws -> AbsolutePath? { + guard triple.isWindows() else { + return nil + } + + guard let (platform, info) = getWindowsPlatformInfo( + swiftSDK: swiftSDK, + environment: environment, + fileSystem: fileSystem + ) else { + return nil + } + + guard let swiftTestingVersion = info.defaults.swiftTestingVersion else { + return nil + } + + let swiftTesting: AbsolutePath = + platform.appending("Developer") + .appending("Library") + .appending("Testing-\(swiftTestingVersion)") + + let binPath: AbsolutePath? = switch triple.arch { + case .x86_64: // amd64 x86_64 x86_64h + swiftTesting.appending("usr") + .appending("bin64") + case .x86: // i386 i486 i586 i686 i786 i886 i986 + swiftTesting.appending("usr") + .appending("bin32") + case .arm: // armv7 and many more + swiftTesting.appending("usr") + .appending("bin32a") + case .aarch64: // aarch6 arm64 + swiftTesting.appending("usr") + .appending("bin64a") + default: + nil + } + + guard let path = binPath, fileSystem.exists(path) else { + return nil + } + + return path + } + public var sdkRootPath: AbsolutePath? { configuration.sdkRootPath } @@ -909,4 +1062,17 @@ public final class UserToolchain: Toolchain { public var xctestPath: AbsolutePath? { configuration.xctestPath } + + public var swiftTestingPathOnWindows: AbsolutePath? { + configuration.swiftTestingPath + } +} + +extension Environment { + fileprivate var windowsSDKRoot: AbsolutePath? { + if let SDKROOT = self["SDKROOT"], let sdkDir = try? AbsolutePath(validating: SDKROOT) { + return sdkDir + } + return nil + } } diff --git a/Sources/PackageModel/WindowsToolchainInfo.swift b/Sources/PackageModel/WindowsToolchainInfo.swift index 794ae3b4cab..0e1bcbe8435 100644 --- a/Sources/PackageModel/WindowsToolchainInfo.swift +++ b/Sources/PackageModel/WindowsToolchainInfo.swift @@ -78,6 +78,10 @@ public struct WindowsPlatformInfo { /// specifies the version string of the bundled XCTest. public let xctestVersion: String + /// SWIFT_TESTING_VERSION + /// specifies the version string of the bundled swift-testing. + public let swiftTestingVersion: String? + /// SWIFTC_FLAGS /// Specifies extra flags to pass to swiftc from Swift Package Manager. public let extraSwiftCFlags: [String]? @@ -89,6 +93,7 @@ public struct WindowsPlatformInfo { extension WindowsPlatformInfo.DefaultProperties: Decodable { enum CodingKeys: String, CodingKey { case xctestVersion = "XCTEST_VERSION" + case swiftTestingVersion = "SWIFT_TESTING_VERSION" case extraSwiftCFlags = "SWIFTC_FLAGS" } }