Generator/Sources/NeedleFramework/Utilities/SourceKittenFramework/library_wrapper.swift (156 lines of code) (raw):
// Copied from SourceKittenFramework library_wrapper.swift [https://github.com/jpsim/SourceKitten/blob/master/Source/SourceKittenFramework/library_wrapper.swift]
//
// library_wrapper.swift
// sourcekitten
//
// Created by Norio Nomura on 2/20/16.
// Copyright © 2016 SourceKitten. All rights reserved.
//
import Foundation
struct DynamicLinkLibrary {
let path: String
let handle: UnsafeMutableRawPointer
func load<T>(symbol: String) -> T {
if let sym = dlsym(handle, symbol) {
return unsafeBitCast(sym, to: T.self)
}
let errorString = String(validatingUTF8: dlerror())
fatalError("Finding symbol \(symbol) failed: \(errorString ?? "unknown error")")
}
}
#if os(Linux)
let toolchainLoader = Loader(searchPaths: [
linuxSourceKitLibPath,
linuxFindSwiftenvActiveLibPath,
linuxFindSwiftInstallationLibPath,
linuxDefaultLibPath
].compactMap({ $0 }))
#else
let toolchainLoader = Loader(searchPaths: [
xcodeDefaultToolchainOverride,
toolchainDir,
xcrunFindPath,
/*
These search paths are used when `xcode-select -p` points to
"Command Line Tools OS X for Xcode", but Xcode.app exists.
*/
applicationsDir?.xcodeDeveloperDir.toolchainDir,
applicationsDir?.xcodeBetaDeveloperDir.toolchainDir,
userApplicationsDir?.xcodeDeveloperDir.toolchainDir,
userApplicationsDir?.xcodeBetaDeveloperDir.toolchainDir
].compactMap { path in
if let fullPath = path?.usrLibDir, fullPath.isFile {
return fullPath
}
return nil
})
#endif
struct Loader {
let searchPaths: [String]
func load(path: String) -> DynamicLinkLibrary {
let fullPaths = searchPaths.map { $0.appending(pathComponent: path) }.filter { $0.isFile }
// try all fullPaths that contains target file,
// then try loading with simple path that depends resolving to DYLD
for fullPath in fullPaths + [path] {
if let handle = dlopen(fullPath, RTLD_LAZY) {
return DynamicLinkLibrary(path: path, handle: handle)
}
}
fatalError("Loading \(path) failed")
}
}
private func env(_ name: String) -> String? {
return ProcessInfo.processInfo.environment[name]
}
/// Run a process at the given (absolute) path, capture output, return outupt.
private func runCommand(_ path: String, _ args: String...) -> String? {
let process = Process()
process.launchPath = path
process.arguments = args
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
guard let encoded = String(data: data, encoding: String.Encoding.utf8) else {
return nil
}
let trimmed = encoded.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if trimmed.isEmpty {
return nil
}
return trimmed
}
/// Returns "LINUX_SOURCEKIT_LIB_PATH" environment variable.
internal let linuxSourceKitLibPath = env("LINUX_SOURCEKIT_LIB_PATH")
/// If available, uses `swiftenv` to determine the user's active Swift root.
internal let linuxFindSwiftenvActiveLibPath: String? = {
guard let swiftenvPath = runCommand("/usr/bin/which", "swiftenv") else {
return nil
}
guard let swiftenvRoot = runCommand(swiftenvPath, "prefix") else {
return nil
}
return swiftenvRoot + "/usr/lib"
}()
/// Attempts to discover the location of libsourcekitdInProc.so by looking at
/// the `swift` binary on the path.
internal let linuxFindSwiftInstallationLibPath: String? = {
guard let swiftPath = runCommand("/usr/bin/which", "swift") else {
return nil
}
if linuxSourceKitLibPath == nil && linuxFindSwiftenvActiveLibPath == nil &&
swiftPath.hasSuffix("/shims/swift") {
/// If we hit this path, the user is invoking Swift via swiftenv shims and has not set the
/// environment variable; this means we're going to end up trying to load from `/usr/lib`
/// which will fail - and instead, we can give a more useful error message.
fatalError("Swift is installed via swiftenv but swiftenv is not initialized.")
}
if !swiftPath.hasSuffix("/bin/swift") {
return nil
}
/// .../bin/swift -> .../lib
return swiftPath.deleting(lastPathComponents: 2).appending(pathComponent: "/lib")
}()
/// Fallback path on Linux if no better option is available.
internal let linuxDefaultLibPath = "/usr/lib"
/// Returns "XCODE_DEFAULT_TOOLCHAIN_OVERRIDE" environment variable
///
/// `launch-with-toolchain` sets the toolchain path to the
/// "XCODE_DEFAULT_TOOLCHAIN_OVERRIDE" environment variable.
private let xcodeDefaultToolchainOverride = env("XCODE_DEFAULT_TOOLCHAIN_OVERRIDE")
/// Returns "TOOLCHAIN_DIR" environment variable
///
/// `Xcode`/`xcodebuild` sets the toolchain path to the
/// "TOOLCHAIN_DIR" environment variable.
private let toolchainDir = env("TOOLCHAIN_DIR")
/// Returns toolchain directory that parsed from result of `xcrun -find swift`
///
/// This is affected by "DEVELOPER_DIR", "TOOLCHAINS" environment variables.
private let xcrunFindPath: String? = {
let pathOfXcrun = "/usr/bin/xcrun"
if !FileManager.default.isExecutableFile(atPath: pathOfXcrun) {
return nil
}
let task = Process()
task.launchPath = pathOfXcrun
task.arguments = ["-find", "swift"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch() // if xcode-select does not exist, crash with `NSInvalidArgumentException`.
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8) else {
return nil
}
var start = output.startIndex
var end = output.startIndex
var contentsEnd = output.startIndex
output.getLineStart(&start, end: &end, contentsEnd: &contentsEnd, for: start..<start)
let xcrunFindSwiftPath = String(output[start..<contentsEnd])
guard xcrunFindSwiftPath.hasSuffix("/usr/bin/swift") else {
return nil
}
let xcrunFindPath = xcrunFindSwiftPath.deleting(lastPathComponents: 3)
// Return nil if xcrunFindPath points to "Command Line Tools OS X for Xcode"
// because it doesn't contain `sourcekitd.framework`.
if xcrunFindPath == "/Library/Developer/CommandLineTools" {
return nil
}
return xcrunFindPath
}()
private let applicationsDir: String? =
NSSearchPathForDirectoriesInDomains(.applicationDirectory, .systemDomainMask, true).first
private let userApplicationsDir: String? =
NSSearchPathForDirectoriesInDomains(.applicationDirectory, .userDomainMask, true).first
private extension String {
var toolchainDir: String {
return appending(pathComponent: "Toolchains/XcodeDefault.xctoolchain")
}
var xcodeDeveloperDir: String {
return appending(pathComponent: "Xcode.app/Contents/Developer")
}
var xcodeBetaDeveloperDir: String {
return appending(pathComponent: "Xcode-beta.app/Contents/Developer")
}
var usrLibDir: String {
return appending(pathComponent: "/usr/lib")
}
func appending(pathComponent: String) -> String {
return URL(fileURLWithPath: self).appendingPathComponent(pathComponent).path
}
func deleting(lastPathComponents numberOfPathComponents: Int) -> String {
var url = URL(fileURLWithPath: self)
for _ in 0..<numberOfPathComponents {
url = url.deletingLastPathComponent()
}
return url.path
}
}
extension String {
internal var isFile: Bool {
return FileManager.default.fileExists(atPath: self)
}
}