Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PackageModel] Support swift-testing installed in a toolchain #7840

Merged
merged 6 commits into from
Aug 5, 2024
Merged
7 changes: 5 additions & 2 deletions Sources/Commands/Utilities/TestingSupport.swift
Original file line number Diff line number Diff line change
@@ -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
14 changes: 11 additions & 3 deletions Sources/PackageModel/Toolchain.swift
Original file line number Diff line number Diff line change
@@ -74,20 +74,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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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)
}
}

@@ -110,4 +114,8 @@ extension Toolchain {
public var extraSwiftCFlags: [String] {
extraFlags.swiftCompilerFlags
}

package static func toolchainLibDir(swiftCompilerPath: AbsolutePath) throws -> AbsolutePath {
try AbsolutePath(validating: "../../lib", relativeTo: resolveSymlinks(swiftCompilerPath))
}
}
8 changes: 7 additions & 1 deletion Sources/PackageModel/ToolchainConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

252 changes: 213 additions & 39 deletions Sources/PackageModel/UserToolchain.swift
Original file line number Diff line number Diff line change
@@ -403,6 +403,48 @@ 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] {
// If this is CommandLineTools all we need to add is a frameworks path.
if let frameworksPath = try? AbsolutePath(
validating: "../../Library/Developer/Frameworks",
relativeTo: resolveSymlinks(derivedSwiftCompiler).parentDirectory
), fileSystem.exists(frameworksPath.appending("Testing.framework")) {
return ["-F", frameworksPath.pathString]
}

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(toolchainLibDir), 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 +462,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 +487,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/<Project>-[VERSION]/...
// Library/Developer/Platforms/[PLATFORM].platform/Developer/SDKs/[PLATFORM].sdk/...
//
// SDKROOT points to [PLATFORM].sdk
@@ -455,14 +498,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 +521,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 +540,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we limit to the testing framework in use by any chance?

}
}

@@ -596,15 +664,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 +705,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,14 +761,28 @@ 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,
swiftCompilerFlags: self.extraFlags.swiftCompilerFlags,
swiftCompilerEnvironment: environment,
swiftPMLibrariesLocation: swiftPMLibrariesLocation,
sdkRootPath: self.swiftSDK.pathsConfiguration.sdkRootPath,
xctestPath: xctestPath
xctestPath: xctestPath,
swiftTestingPath: swiftTestingPath
)

self.fileSystem = fileSystem
@@ -786,6 +878,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/<Project>-[VERSION]/...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really use Library/Developer directory naming on Windows? I thought this was very Darwin-specific

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think so since this was the original comment I moved, that's how we discover XCTest today.

// 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 +930,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 +996,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
}
@@ -910,6 +1071,10 @@ public final class UserToolchain: Toolchain {
configuration.xctestPath
}

public var swiftTestingPathOnWindows: AbsolutePath? {
configuration.swiftTestingPath
}

private static func loadJSONResource<T: Decodable>(
config: AbsolutePath, type: T.Type, `default`: T
)
@@ -925,3 +1090,12 @@ public final class UserToolchain: Toolchain {
return `default`
}
}

extension Environment {
fileprivate var windowsSDKRoot: AbsolutePath? {
if let SDKROOT = self["SDKROOT"], let sdkDir = try? AbsolutePath(validating: SDKROOT) {
return sdkDir
}
return nil
}
}
5 changes: 5 additions & 0 deletions Sources/PackageModel/WindowsToolchainInfo.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}