From 9ee1649e9837c261458b5503347d5ad8ec50201d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 26 Mar 2024 15:00:03 -0400 Subject: [PATCH] On Darwin, allow XCTest to be missing if we're only building swift-testing tests. (#7426) This PR removes the constraint on Darwin that XCTest.framework must be present in order to build tests using swift-testing. On Darwin, XCTest is included as a framework inside Xcode, but if a developer installs the Xcode Command Line Tools instead of the full IDE, XCTest is not included. They then get a diagnostic of the form: > error: XCTest not available: terminated(1): /usr/bin/xcrun --sdk macosx --show-sdk-platform-path output: > xcrun: error: unable to lookup item 'PlatformPath' from command line tools installation > xcrun: error: unable to lookup item 'PlatformPath' in SDK '/Library/Developer/CommandLineTools/SDKs/MacOSX14.0.sdk' Which is a poor experience if they aren't even using XCTest. This change, as a (positive) side effect, suppresses the same diagnostic when running commands that are not usually dependent on the presence of XCTest such as `swift build`. Note that swift-corelibs-xctest is not supported on Darwin, so installing the Xcode Command Line Tools and adding an explicit dependency on swift-corelibs-xctest will not produce a functional test target bundle. Supporting swift-corelibs-xctest on Darwin is a potential future direction. Automated testing for this change is difficult because it relies on a build environment that is not supported in CI (namely the presence of the CL tools but not Xcode nor XCTest.framework.) I have manually tested the change against swift-testing's own test target. A separate PR will be necessary in swift-testing to remove some remaining XCTest dependencies. Those changes are not covered by this PR. Resolves rdar://125372431. --- Sources/Commands/SwiftTestCommand.swift | 22 +++++-- .../Commands/Utilities/TestingSupport.swift | 11 ++-- Sources/PackageModel/SwiftSDKs/SwiftSDK.swift | 64 ++++++++++++++++--- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift index bbccc0c2260..bd2ebf02208 100644 --- a/Sources/Commands/SwiftTestCommand.swift +++ b/Sources/Commands/SwiftTestCommand.swift @@ -40,7 +40,8 @@ private enum TestError: Swift.Error { case testProductNotFound(productName: String) case productIsNotTest(productName: String) case multipleTestProducts([String]) - case xctestNotAvailable + case xctestNotAvailable(reason: String) + case xcodeNotInstalled } extension TestError: CustomStringConvertible { @@ -57,8 +58,10 @@ extension TestError: CustomStringConvertible { return "invalid list test JSON structure, produced by \(context)\(underlying)" case .multipleTestProducts(let products): return "found multiple test products: \(products.joined(separator: ", ")); use --test-product to select one" - case .xctestNotAvailable: - return "XCTest not available" + case let .xctestNotAvailable(reason): + return "XCTest not available: \(reason)" + case .xcodeNotInstalled: + return "XCTest not available; download and install Xcode to use XCTest on this platform" } } } @@ -203,9 +206,14 @@ package struct SwiftTestCommand: AsyncSwiftCommand { private func xctestRun(_ swiftCommandState: SwiftCommandState) async throws { // validate XCTest available on darwin based systems let toolchain = try swiftCommandState.getTargetToolchain() - let isHostTestingAvailable = try swiftCommandState.getHostToolchain().swiftSDK.supportsTesting - if (toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil) || !isHostTestingAvailable { - throw TestError.xctestNotAvailable + if case let .unsupported(reason) = try swiftCommandState.getHostToolchain().swiftSDK.xctestSupport { + if let reason { + throw TestError.xctestNotAvailable(reason: reason) + } else { + throw TestError.xcodeNotInstalled + } + } else if toolchain.targetTriple.isDarwin() && toolchain.xctestPath == nil { + throw TestError.xcodeNotInstalled } let buildParameters = try swiftCommandState.buildParametersForTest(options: self.options, library: .xctest) @@ -814,7 +822,7 @@ final class TestRunner { #if os(macOS) if library == .xctest { guard let xctestPath = self.toolchain.xctestPath else { - throw TestError.xctestNotAvailable + throw TestError.xcodeNotInstalled } args = [xctestPath.pathString] args += additionalArguments diff --git a/Sources/Commands/Utilities/TestingSupport.swift b/Sources/Commands/Utilities/TestingSupport.swift index 43f889fc309..d7643d330e7 100644 --- a/Sources/Commands/Utilities/TestingSupport.swift +++ b/Sources/Commands/Utilities/TestingSupport.swift @@ -178,11 +178,12 @@ enum TestingSupport { #endif return env #else - // Add the sdk platform path if we have it. If this is not present, we might always end up failing. - let sdkPlatformFrameworksPath = try SwiftSDK.sdkPlatformFrameworkPaths() - // appending since we prefer the user setting (if set) to the one we inject - env.appendPath("DYLD_FRAMEWORK_PATH", value: sdkPlatformFrameworksPath.fwk.pathString) - env.appendPath("DYLD_LIBRARY_PATH", value: sdkPlatformFrameworksPath.lib.pathString) + // Add the sdk platform path if we have it. + if let sdkPlatformFrameworksPath = try? SwiftSDK.sdkPlatformFrameworkPaths() { + // appending since we prefer the user setting (if set) to the one we inject + env.appendPath("DYLD_FRAMEWORK_PATH", value: sdkPlatformFrameworksPath.fwk.pathString) + env.appendPath("DYLD_LIBRARY_PATH", value: sdkPlatformFrameworksPath.lib.pathString) + } // Fast path when no sanitizers are enabled. if sanitizers.isEmpty { diff --git a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift index 3a3ec0452f3..665c4fe1351 100644 --- a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift +++ b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift @@ -147,7 +147,29 @@ public struct SwiftSDK: Equatable { public var architectures: [String]? = nil /// Whether or not the receiver supports testing. - public let supportsTesting: Bool + @available(*, deprecated, message: "Use `xctestSupport` instead") + public var supportsTesting: Bool { + if case .supported = xctestSupport { + return true + } + return false + } + + /// Whether or not the receiver supports testing using XCTest. + package enum XCTestSupport: Sendable, Equatable { + /// XCTest is supported. + case supported + + /// XCTest is not supported. + /// + /// - Parameters: + /// - reason: A string explaining why XCTest is not supported. If + /// `nil`, no additional information is available. + case unsupported(reason: String?) + } + + /// Whether or not the receiver supports using XCTest. + package let xctestSupport: XCTestSupport /// Root directory path of the SDK used to compile for the target triple. @available(*, deprecated, message: "use `pathsConfiguration.sdkRootPath` instead") @@ -418,18 +440,43 @@ public struct SwiftSDK: Equatable { } /// Creates a Swift SDK with the specified properties. + @available(*, deprecated, message: "use `init(hostTriple:targetTriple:toolset:pathsConfiguration:xctestSupport:)` instead") public init( hostTriple: Triple? = nil, targetTriple: Triple? = nil, toolset: Toolset, pathsConfiguration: PathsConfiguration, - supportsTesting: Bool = true + supportsTesting: Bool + ) { + let xctestSupport: XCTestSupport + if supportsTesting { + xctestSupport = .supported + } else { + xctestSupport = .unsupported(reason: nil) + } + + self.init( + hostTriple: hostTriple, + targetTriple: targetTriple, + toolset: toolset, + pathsConfiguration: pathsConfiguration, + xctestSupport: xctestSupport + ) + } + + /// Creates a Swift SDK with the specified properties. + package init( + hostTriple: Triple? = nil, + targetTriple: Triple? = nil, + toolset: Toolset, + pathsConfiguration: PathsConfiguration, + xctestSupport: XCTestSupport = .supported ) { self.hostTriple = hostTriple self.targetTriple = targetTriple self.toolset = toolset self.pathsConfiguration = pathsConfiguration - self.supportsTesting = supportsTesting + self.xctestSupport = xctestSupport } /// Returns the bin directory for the host. @@ -496,7 +543,7 @@ public struct SwiftSDK: Equatable { #endif // Compute common arguments for clang and swift. - let supportsTesting: Bool + let xctestSupport: XCTestSupport var extraCCFlags: [String] = [] var extraSwiftCFlags: [String] = [] #if os(macOS) @@ -506,13 +553,12 @@ public struct SwiftSDK: Equatable { extraSwiftCFlags += ["-F", sdkPaths.fwk.pathString] extraSwiftCFlags += ["-I", sdkPaths.lib.pathString] extraSwiftCFlags += ["-L", sdkPaths.lib.pathString] - supportsTesting = true + xctestSupport = .supported } catch { - supportsTesting = false - observabilityScope?.emit(warning: "could not determine XCTest paths: \(error)") + xctestSupport = .unsupported(reason: String(describing: error)) } #else - supportsTesting = true + xctestSupport = .supported #endif #if !os(Windows) @@ -528,7 +574,7 @@ public struct SwiftSDK: Equatable { rootPaths: [binDir] ), pathsConfiguration: .init(sdkRootPath: sdkPath), - supportsTesting: supportsTesting + xctestSupport: xctestSupport ) }