diff --git a/CHANGELOG.md b/CHANGELOG.md index b6251d83..ebc5820c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## Main + +#### Breaking + +* None. + +#### Enhancements + +* Include each failed `dlopen`/`LoadLibrary` attempt and the underlying + `dlerror()` / `GetLastError()` reason in the fatal error message emitted + when `sourcekitdInProc` cannot be loaded. Previously the error was just + `Loading X failed`, which forced downstream users (e.g. SwiftLint) to + guess at the root cause; common failures such as architecture mismatch + (`mach-o file, but is an incompatible architecture`) are now surfaced + directly. + [Daniel Sunarjo](https://github.com/sunarjodaniel) + [realm/SwiftLint#6475](https://github.com/realm/SwiftLint/issues/6475) + +#### Bug Fixes + +* None. + ## 0.37.3 #### Breaking diff --git a/Source/SourceKittenFramework/library_wrapper.swift b/Source/SourceKittenFramework/library_wrapper.swift index c300e249..4f3a07e6 100644 --- a/Source/SourceKittenFramework/library_wrapper.swift +++ b/Source/SourceKittenFramework/library_wrapper.swift @@ -43,19 +43,36 @@ struct Loader { // try all fullPaths that contains target file, // then try loading with simple path that depends resolving to DYLD + var attemptFailures: [String] = [] for fullPath in fullPaths + [path] { #if os(Windows) if let handle = fullPath.withCString(encodedAs: UTF16.self, LoadLibraryW) { return DynamicLinkLibrary(handle: handle) } + attemptFailures.append(" \(fullPath): GetLastError=\(GetLastError())") #else if let handle = dlopen(fullPath, RTLD_LAZY) { return DynamicLinkLibrary(handle: handle) } + // dlerror() must be read immediately after a failed dlopen; it clears on read. + let reason = dlerror().flatMap { String(validatingUTF8: $0) } ?? "" + attemptFailures.append(" \(fullPath): \(reason)") #endif } - fatalError("Loading \(path) failed") + fatalError(Loader.failureMessage(path: path, attemptFailures: attemptFailures)) + } + + /// Builds the diagnostic emitted when no candidate path could be loaded. + /// Extracted so the message can be exercised in tests without triggering a fatal error. + static func failureMessage(path: String, attemptFailures: [String]) -> String { + guard !attemptFailures.isEmpty else { + return "Loading \(path) failed: no candidate paths were attempted (check searchPaths configuration)." + } + return """ + Loading \(path) failed. Tried: + \(attemptFailures.joined(separator: "\n")) + """ } } diff --git a/Tests/SourceKittenFrameworkTests/LoaderTests.swift b/Tests/SourceKittenFrameworkTests/LoaderTests.swift new file mode 100644 index 00000000..5922082e --- /dev/null +++ b/Tests/SourceKittenFrameworkTests/LoaderTests.swift @@ -0,0 +1,30 @@ +@testable import SourceKittenFramework +import XCTest + +final class LoaderTests: XCTestCase { + + func testFailureMessageIncludesEveryAttemptAndItsReason() { + let message = Loader.failureMessage( + path: "sourcekitdInProc.framework/Versions/A/sourcekitdInProc", + attemptFailures: [ + " /Xcode.app/.../usr/lib/sourcekitdInProc.framework/Versions/A/sourcekitdInProc: " + + "mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')", + " sourcekitdInProc.framework/Versions/A/sourcekitdInProc: image not found" + ] + ) + + // The path being loaded is named. + XCTAssertTrue(message.contains("sourcekitdInProc.framework/Versions/A/sourcekitdInProc")) + // Every attempt's dlerror() reason survives into the final message. + XCTAssertTrue(message.contains("incompatible architecture")) + XCTAssertTrue(message.contains("image not found")) + } + + func testFailureMessageWhenNoCandidatesWereTried() { + // Empty searchPaths *and* an empty bare path attempt should produce an actionable hint + // rather than a bare "Loading X failed" with no further detail. + let message = Loader.failureMessage(path: "libfoo.so", attemptFailures: []) + XCTAssertTrue(message.contains("libfoo.so")) + XCTAssertTrue(message.contains("searchPaths")) + } +}