From 0884fb1f3920dbbf9d316ccf7d50339f7c0abba7 Mon Sep 17 00:00:00 2001 From: Ryan Mansfield Date: Mon, 10 Nov 2025 20:15:47 -0500 Subject: [PATCH] Warn when SWIFT_EXEC or SWIFT_EXEC_MANIFEST point to invalid paths. Previously when SWIFT_EXEC or SWIFT_EXEC_MANIFEST were set to non-existent or non-executable paths, swiftpm would silently ignore them and fall back to the default Swift compiler. This caused confusion during development when typos or incorrect paths were used. --- Sources/CoreCommands/SwiftCommandState.swift | 2 + Sources/PackageModel/UserToolchain.swift | 34 +++++++- .../PackageModelTests/PackageModelTests.swift | 87 +++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index c927160d89a..459b303ce01 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -1041,6 +1041,7 @@ public final class SwiftCommandState { swiftSDK: swiftSDK, environment: self.environment, customTargetInfo: targetInfo, + observabilityScope: self.observabilityScope, fileSystem: self.fileSystem) }) }() @@ -1057,6 +1058,7 @@ public final class SwiftCommandState { swiftSDK: hostSwiftSDK, environment: self.environment, customTargetInfo: targetInfo, + observabilityScope: self.observabilityScope, fileSystem: self.fileSystem ) }) diff --git a/Sources/PackageModel/UserToolchain.swift b/Sources/PackageModel/UserToolchain.swift index 2520787428b..ec4d8d05d5a 100644 --- a/Sources/PackageModel/UserToolchain.swift +++ b/Sources/PackageModel/UserToolchain.swift @@ -339,7 +339,8 @@ public final class UserToolchain: Toolchain { useXcrun: Bool, environment: Environment, searchPaths: [AbsolutePath], - fileSystem: any FileSystem + fileSystem: any FileSystem, + observabilityScope: ObservabilityScope? = nil ) throws -> SwiftCompilers { func validateCompiler(at path: AbsolutePath?) throws { guard let path else { return } @@ -351,10 +352,37 @@ public final class UserToolchain: Toolchain { } let lookup = { UserToolchain.lookup(variable: $0, searchPaths: searchPaths, environment: environment) } + + // Warn if SWIFT_EXEC or SWIFT_EXEC_MANIFEST is set but points to a non-existent or non-executable path + func warnIfInvalid(envVar: String, value: String, resolved: AbsolutePath?) { + guard resolved == nil else { return } + + let message: String + if let absolutePath = try? AbsolutePath(validating: value) { + if fileSystem.exists(absolutePath) { + message = "\(envVar) is set to '\(value)' which exists but is not executable; ignoring" + } else { + message = "\(envVar) is set to '\(value)' but the file does not exist; ignoring" + } + } else { + message = "\(envVar) is set to '\(value)' but no executable was found in search paths; ignoring" + } + + observabilityScope?.emit(warning: message) + } + // Get overrides. let SWIFT_EXEC_MANIFEST = lookup("SWIFT_EXEC_MANIFEST") let SWIFT_EXEC = lookup("SWIFT_EXEC") + // Emit warnings if environment variables are set but lookup failed + if let swiftExecValue = environment["SWIFT_EXEC"], !swiftExecValue.isEmpty { + warnIfInvalid(envVar: "SWIFT_EXEC", value: swiftExecValue, resolved: SWIFT_EXEC) + } + if let swiftExecManifestValue = environment["SWIFT_EXEC_MANIFEST"], !swiftExecManifestValue.isEmpty { + warnIfInvalid(envVar: "SWIFT_EXEC_MANIFEST", value: swiftExecManifestValue, resolved: SWIFT_EXEC_MANIFEST) + } + // Validate the overrides. try validateCompiler(at: SWIFT_EXEC) try validateCompiler(at: SWIFT_EXEC_MANIFEST) @@ -693,6 +721,7 @@ public final class UserToolchain: Toolchain { customTargetInfo: JSON? = nil, customLibrariesLocation: ToolchainConfiguration.SwiftPMLibrariesLocation? = nil, customInstalledSwiftPMConfiguration: InstalledSwiftPMConfiguration? = nil, + observabilityScope: ObservabilityScope? = nil, fileSystem: any FileSystem = localFileSystem ) throws { self.swiftSDK = swiftSDK @@ -716,7 +745,8 @@ public final class UserToolchain: Toolchain { useXcrun: self.useXcrun, environment: environment, searchPaths: self.envSearchPaths, - fileSystem: fileSystem + fileSystem: fileSystem, + observabilityScope: observabilityScope ) self.swiftCompilerPath = swiftCompilers.compile self.architectures = swiftSDK.architectures diff --git a/Tests/PackageModelTests/PackageModelTests.swift b/Tests/PackageModelTests/PackageModelTests.swift index 9d230b286d5..a23e3f116dd 100644 --- a/Tests/PackageModelTests/PackageModelTests.swift +++ b/Tests/PackageModelTests/PackageModelTests.swift @@ -15,6 +15,7 @@ import Basics @_spi(SwiftPMInternal) @testable import PackageModel +import _InternalTestSupport import func TSCBasic.withTemporaryFile import XCTest @@ -199,4 +200,90 @@ final class PackageModelTests: XCTestCase { XCTAssertEqual(compilers.compile, binDirs.first?.appending(expectedExecuable)) } } + + func testDetermineSwiftCompilersWarnsOnInvalidSWIFT_EXEC() throws { + let fs = localFileSystem + try withTemporaryDirectory(removeTreeOnDeinit: true) { tmp in + let toolchainPath = tmp.appending("swift.xctoolchain") + let toolchainBinDir = toolchainPath.appending(components: "usr", "bin") + try fs.createDirectory(toolchainBinDir, recursive: true) + + #if os(Windows) + let exeSuffix = ".exe" + #else + let exeSuffix = "" + #endif + + // Create a valid swiftc in the toolchain + let validSwiftc = toolchainBinDir.appending("swiftc\(exeSuffix)") + try fs.writeFileContents(validSwiftc, bytes: ByteString(Self.tinyPEBytes)) + #if !os(Windows) + try fs.chmod(.executable, path: validSwiftc, options: []) + #endif + + // Test 1: SWIFT_EXEC points to non-existent file + do { + let observability = ObservabilitySystem.makeForTesting() + let nonExistentPath = tmp.appending(components: "nonexistent", "path", "to", "swiftc") + let environment: Environment = ["SWIFT_EXEC": nonExistentPath.pathString] + + _ = try UserToolchain.determineSwiftCompilers( + binDirectories: [toolchainBinDir], + useXcrun: false, + environment: environment, + searchPaths: [], + fileSystem: fs, + observabilityScope: observability.topScope + ) + + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("SWIFT_EXEC is set to '\(nonExistentPath.pathString)' but the file does not exist; ignoring"), severity: .warning) + } + } + + // Test 2: SWIFT_EXEC points to file that exists but is not executable + do { + let observability = ObservabilitySystem.makeForTesting() + let notExecutablePath = tmp.appending("not-executable") + try fs.writeFileContents(notExecutablePath, bytes: "") + #if !os(Windows) + try fs.chmod(.userUnWritable, path: notExecutablePath, options: []) + #endif + + let environment: Environment = ["SWIFT_EXEC": notExecutablePath.pathString] + + _ = try UserToolchain.determineSwiftCompilers( + binDirectories: [toolchainBinDir], + useXcrun: false, + environment: environment, + searchPaths: [], + fileSystem: fs, + observabilityScope: observability.topScope + ) + + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("SWIFT_EXEC is set to '\(notExecutablePath.pathString)' which exists but is not executable; ignoring"), severity: .warning) + } + } + + // Test 3: SWIFT_EXEC is not an absolute path and not found in search paths + do { + let observability = ObservabilitySystem.makeForTesting() + let environment: Environment = ["SWIFT_EXEC": "nonexistent-compiler"] + + _ = try UserToolchain.determineSwiftCompilers( + binDirectories: [toolchainBinDir], + useXcrun: false, + environment: environment, + searchPaths: [], + fileSystem: fs, + observabilityScope: observability.topScope + ) + + testDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: .contains("SWIFT_EXEC is set to 'nonexistent-compiler' but no executable was found in search paths; ignoring"), severity: .warning) + } + } + } + } }