From 62397132ec952844f7122d89f29c22155202c465 Mon Sep 17 00:00:00 2001 From: Daniel Sunarjo Date: Tue, 19 May 2026 16:27:48 +0700 Subject: [PATCH] Surface dlerror/GetLastError reason on Loader load failure When the Darwin/Linux/Windows loaders fail to dlopen sourcekitdInProc (or libsourcekitdInProc.so / sourcekitdInProc.dll), they currently discard the underlying error and trap with the bare message "Loading X failed". That message tells the user nothing actionable, and the failure has many possible root causes (toolchain not found, architecture mismatch when running through Rosetta, permission, etc). Capture each candidate path that was tried and the OS-reported reason (dlerror() on POSIX, GetLastError() on Windows), and include the full list in the fatalError message. The successful-load path is unchanged. This is the kind of failure SwiftLint users have been hitting on Xcode 26+ when a SwiftLint build phase ends up running through an x86_64 build chain (e.g. an x86_64 ruby / fastlane host) while Xcode 26 ships sourcekitdInProc.framework as arm64-only. Today they get the bare "Loading X failed" trap; after this change they'd see "mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')" pointing straight at the cause. Refs realm/SwiftLint#6475 --- CHANGELOG.md | 22 ++++++++++++++ .../library_wrapper.swift | 19 +++++++++++- .../LoaderTests.swift | 30 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 Tests/SourceKittenFrameworkTests/LoaderTests.swift 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")) + } +}