| //===------- SwiftcInvocation.swift - Utilities for invoking swiftc -------===// |
| // |
| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See https://swift.org/LICENSE.txt for license information |
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| //===----------------------------------------------------------------------===// |
| // This file provides the logic for invoking swiftc to parse Swift files. |
| //===----------------------------------------------------------------------===// |
| |
| import Foundation |
| |
| #if os(macOS) |
| import Darwin |
| #elseif os(Linux) |
| import Glibc |
| #endif |
| |
| /// The result of process execution, containing the exit status code, |
| /// stdout, and stderr |
| struct ProcessResult { |
| /// The process exit code. A non-zero exit code usually indicates failure. |
| let exitCode: Int |
| |
| /// The contents of the process's stdout as Data. |
| let stdoutData: Data |
| |
| /// The contents of the process's stderr as Data. |
| let stderrData: Data |
| |
| /// The contents of the process's stdout, assuming the data was UTF-8 encoded. |
| var stdout: String { |
| return String(data: stdoutData, encoding: .utf8)! |
| } |
| |
| /// The contents of the process's stderr, assuming the data was UTF-8 encoded. |
| var stderr: String { |
| return String(data: stderrData, encoding: .utf8)! |
| } |
| |
| /// Whether or not this process had a non-zero exit code. |
| var wasSuccessful: Bool { |
| return exitCode == 0 |
| } |
| } |
| |
| /// Runs the provided executable with the provided arguments and returns the |
| /// contents of stdout and stderr as Data. |
| /// - Parameters: |
| /// - executable: The full file URL to the executable you're running. |
| /// - arguments: A list of strings to pass to the process as arguments. |
| /// - Returns: A ProcessResult containing stdout, stderr, and the exit code. |
| func run(_ executable: URL, arguments: [String] = []) -> ProcessResult { |
| // Use an autoreleasepool to prevent memory- and file-descriptor leaks. |
| return autoreleasepool { |
| () -> ProcessResult in |
| |
| let stdoutPipe = Pipe() |
| var stdoutData = Data() |
| stdoutPipe.fileHandleForReading.readabilityHandler = { file in |
| stdoutData.append(file.availableData) |
| } |
| |
| let stderrPipe = Pipe() |
| var stderrData = Data() |
| stderrPipe.fileHandleForReading.readabilityHandler = { file in |
| stderrData.append(file.availableData) |
| } |
| |
| let process = Process() |
| |
| process.terminationHandler = { process in |
| stdoutPipe.fileHandleForReading.readabilityHandler = nil |
| stderrPipe.fileHandleForReading.readabilityHandler = nil |
| } |
| |
| process.launchPath = executable.path |
| process.arguments = arguments |
| process.standardOutput = stdoutPipe |
| process.standardError = stderrPipe |
| process.launch() |
| process.waitUntilExit() |
| return ProcessResult(exitCode: Int(process.terminationStatus), |
| stdoutData: stdoutData, |
| stderrData: stderrData) |
| } |
| } |
| |
| /// Finds the dylib or executable which the provided address falls in. |
| /// - Parameter dsohandle: A pointer to a symbol in the object file you're |
| /// looking for. If not provided, defaults to the |
| /// caller's `#dsohandle`, which will give you the |
| /// object file the caller resides in. |
| /// - Returns: A File URL pointing to the object where the provided address |
| /// resides. This may be a dylib, shared object, static library, |
| /// or executable. If unable to find the appropriate object, returns |
| /// `nil`. |
| func findFirstObjectFile(for dsohandle: UnsafeRawPointer = #dsohandle) -> URL? { |
| var info = dl_info() |
| if dladdr(dsohandle, &info) == 0 { |
| return nil |
| } |
| let path = String(cString: info.dli_fname) |
| return URL(fileURLWithPath: path) |
| } |
| |
| enum InvocationError: Error { |
| case couldNotFindSwiftc |
| case couldNotFindSDK |
| case abort(code: Int) |
| } |
| |
| struct SwiftcRunner { |
| /// Gets the `swiftc` binary packaged alongside this library. |
| /// - Returns: The path to `swiftc` relative to the path of this library |
| /// file, or `nil` if it could not be found. |
| /// - Note: This makes assumptions about your Swift installation directory |
| /// structure. Importantly, it assumes that the directory tree is |
| /// shaped like this: |
| /// ``` |
| /// install_root/ |
| /// - bin/ |
| /// - swiftc |
| /// - lib/ |
| /// - swift/ |
| /// - ${target}/ |
| /// - libswiftSwiftSyntax.[dylib|so] |
| /// ``` |
| static func locateSwiftc() -> URL? { |
| guard let libraryPath = findFirstObjectFile() else { return nil } |
| let swiftcURL = libraryPath.deletingLastPathComponent() |
| .deletingLastPathComponent() |
| .deletingLastPathComponent() |
| .deletingLastPathComponent() |
| .appendingPathComponent("bin") |
| .appendingPathComponent("swiftc") |
| guard FileManager.default.fileExists(atPath: swiftcURL.path) else { |
| return nil |
| } |
| return swiftcURL |
| } |
| |
| #if os(macOS) |
| /// The location of the macOS SDK, or `nil` if it could not be found. |
| static let macOSSDK: String? = { |
| let url = URL(fileURLWithPath: "/usr/bin/env") |
| let result = run(url, arguments: ["xcrun", "--show-sdk-path"]) |
| guard result.wasSuccessful else { return nil } |
| let toolPath = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines) |
| if toolPath.isEmpty { return nil } |
| return toolPath |
| }() |
| #endif |
| |
| /// Internal static cache of the Swiftc path. |
| static let _swiftcURL: URL? = SwiftcRunner.locateSwiftc() |
| |
| /// The URL where the `swiftc` binary lies. |
| let swiftcURL: URL |
| |
| /// The source file being parsed. |
| let sourceFile: URL |
| |
| /// Creates a SwiftcRunner that will parse and emit the syntax |
| /// tree for a provided source file. |
| /// - Parameter sourceFile: The URL to the source file you're trying |
| /// to parse. |
| init(sourceFile: URL) throws { |
| guard let url = SwiftcRunner._swiftcURL else { |
| throw InvocationError.couldNotFindSwiftc |
| } |
| self.swiftcURL = url |
| self.sourceFile = sourceFile |
| } |
| |
| /// Invokes swiftc with the provided arguments. |
| func invoke() throws -> ProcessResult { |
| var arguments = ["-frontend", "-emit-syntax"] |
| arguments.append(sourceFile.path) |
| #if os(macOS) |
| guard let sdk = SwiftcRunner.macOSSDK else { |
| throw InvocationError.couldNotFindSDK |
| } |
| arguments.append("-sdk") |
| arguments.append(sdk) |
| #endif |
| return run(swiftcURL, arguments: arguments) |
| } |
| } |