Skip to content

Commit

Permalink
Add package build step to SPM docs generation (#614)
Browse files Browse the repository at this point in the history
Final part of jazzy+SPM support: this runs `swift build` as part of generating docs for SPM modules.  This makes sure that the build manifest (a) exists and (b) is consistent with the latest changes to the source code, same as Xcode path.  (No need to do a `clean` so is v. cheap if the package has already been built.)

First commit is a pure refactor of all the popen-ish code because I couldn't stand duplicating it again.  Accounts for most of the changed LOC.

Second commit adds a `Module` initializer that runs `swift build` before going to SourceKit.

I tentatively kept the existing SPM initializer to keep semantics for existing users (SourceDocs uses it, didn't look further).
  • Loading branch information
johnfairh authored and jpsim committed Sep 6, 2019
1 parent ce38246 commit 7e35031
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 247 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
* Support doc generation for modules built with Xcode 11.
[John Fairhurst](https://github.com/johnfairh)

* Add `Module.init?(spmArguments:spmName:inPath)` and use in `doc` commmand
to ensure Swift Package Manager module documentation is up to date.
[John Fairhurst](https://github.com/johnfairh)

##### Bug Fixes

* Fix crash with misplaced documentation comment.
Expand Down
113 changes: 113 additions & 0 deletions Source/SourceKittenFramework/Exec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// Exec.swift
// SourceKittenFramework
//
// Copyright © 2019 SourceKitten. All rights reserved.
//

import Foundation

/// Namespace for utilities to execute a child process.
enum Exec {
/// How to handle stderr output from the child process.
enum Stderr {
/// Treat stderr same as parent process.
case inherit
/// Send stderr to /dev/null.
case discard
/// Merge stderr with stdout.
case merge
}

/// The result of running the child process.
struct Results {
/// The process's exit status.
let terminationStatus: Int32
/// The data from stdout and optionally stderr.
let data: Data
/// The `data` reinterpreted as a string with whitespace trimmed; `nil` for the empty string.
var string: String? {
let encoded = String(data: data, encoding: .utf8) ?? ""
let trimmed = encoded.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}

/**
Run a command with arguments and return its output and exit status.
- parameter command: Absolute path of the command to run.
- parameter arguments: Arguments to pass to the command.
- parameter currentDirectory: Current directory for the command. By default
the parent process's current directory.
- parameter stderr: What to do with stderr output from the command. By default
whatever the parent process does.
*/
static func run(_ command: String,
_ arguments: String...,
currentDirectory: String = FileManager.default.currentDirectoryPath,
stderr: Stderr = .inherit) -> Results {
return run(command, arguments, currentDirectory: currentDirectory, stderr: stderr)
}

/**
Run a command with arguments and return its output and exit status.
- parameter command: Absolute path of the command to run.
- parameter arguments: Arguments to pass to the command.
- parameter currentDirectory: Current directory for the command. By default
the parent process's current directory.
- parameter stderr: What to do with stderr output from the command. By default
whatever the parent process does.
*/
static func run(_ command: String,
_ arguments: [String] = [],
currentDirectory: String = FileManager.default.currentDirectoryPath,
stderr: Stderr = .inherit) -> Results {
let process = Process()
process.arguments = arguments

let pipe = Pipe()
process.standardOutput = pipe

switch stderr {
case .discard:
// FileHandle.nullDevice does not work here, as it consists of an invalid file descriptor,
// causing process.launch() to abort with an EBADF.
process.standardError = FileHandle(forWritingAtPath: "/dev/null")!
case .merge:
process.standardError = pipe
case .inherit:
break
}

do {
#if canImport(Darwin)
if #available(macOS 10.13, *) {
process.executableURL = URL(fileURLWithPath: command)
process.currentDirectoryURL = URL(fileURLWithPath: currentDirectory)
try process.run()
} else {
process.launchPath = command
process.currentDirectoryPath = currentDirectory
process.launch()
}
#elseif compiler(>=5)
process.executableURL = URL(fileURLWithPath: command)
process.currentDirectoryURL = URL(fileURLWithPath: currentDirectory)
try process.run()
#else
process.launchPath = command
process.currentDirectoryPath = currentDirectory
process.launch()
#endif
} catch {
return Results(terminationStatus: -1, data: Data())
}

let file = pipe.fileHandleForReading
let data = file.readDataToEndOfFile()
process.waitUntilExit()
return Results(terminationStatus: process.terminationStatus, data: data)
}
}
34 changes: 31 additions & 3 deletions Source/SourceKittenFramework/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public struct Module {
/**
Failable initializer to create a Module from a Swift Package Manager build record.
Use this initializer when the package has already been built and the `.build` directory exists.
- parameter spmName: Module name. Will use some non-Test module that is part of the
package if `nil`.
- parameter path: Path of the directory containing the SPM `.build` directory.
Expand All @@ -46,7 +48,8 @@ public struct Module {
let yamlPath = URL(fileURLWithPath: path).appendingPathComponent(".build/debug.yaml").path
guard let yaml = try? Yams.compose(yaml: String(contentsOfFile: yamlPath, encoding: .utf8)),
let commands = (yaml as Node?)?["commands"]?.mapping?.values else {
fatalError("SPM build manifest does not exist at `\(yamlPath)` or does not match expected format.")
fputs("SPM build manifest does not exist at `\(yamlPath)` or does not match expected format.", stderr)
return nil
}

func matchModuleName(node: Node) -> Bool {
Expand All @@ -69,7 +72,8 @@ public struct Module {
let otherArguments = moduleCommand["other-args"]?.array(of: String.self),
let sources = moduleCommand["sources"]?.array(of: String.self),
let moduleName = moduleCommand["module-name"]!.string else {
fatalError("SPM build manifest does not match expected format.")
fputs("SPM build manifest '\(yamlPath)` does not match expected format.", stderr)
return nil
}
name = moduleName
compilerArguments = {
Expand All @@ -83,6 +87,30 @@ public struct Module {
sourceFiles = sources
}

/**
Failable initializer to create a Module by building a Swift Package Manager project.
Use this initializer if the package has not been built or may have changed since last built.
- parameter spmArguments: Additional arguments to pass to `swift build`
- parameter spmName: Module name. Will use some non-Test module that is part of the
package if `nil`.
- parameter path: Path of the directory containing the `Package.swift` file.
Uses the current directory by default.
*/
public init?(spmArguments: [String], spmName: String? = nil, inPath path: String = FileManager.default.currentDirectoryPath) {
fputs("Running swift build\n", stderr)
let buildResults = Exec.run("/usr/bin/env", ["swift", "build"] + spmArguments, currentDirectory: path, stderr: .merge)
guard buildResults.terminationStatus == 0 else {
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("swift-build-\(UUID().uuidString).log")
_ = try? buildResults.data.write(to: file)
fputs("Build failed, saved `swift build` log file: \(file.path)\n", stderr)
return nil
}

self.init(spmName: spmName, inPath: path)
}

/**
Failable initializer to create a Module by the arguments necessary pass in to `xcodebuild` to build it.
Optionally pass in a `moduleName` and `path`.
Expand Down Expand Up @@ -121,7 +149,7 @@ public struct Module {
fputs("Could not parse compiler arguments from `xcodebuild` output.\n", stderr)
fputs("Please confirm that `xcodebuild` is building a Swift module.\n", stderr)
let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("xcodebuild-\(NSUUID().uuidString).log")
try! xcodeBuildOutput.data(using: .utf8)?.write(to: file)
_ = try? xcodeBuildOutput.data(using: .utf8)?.write(to: file)
fputs("Saved `xcodebuild` log file: \(file.path)\n", stderr)
return nil
}
Expand Down
75 changes: 5 additions & 70 deletions Source/SourceKittenFramework/Xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,45 +50,10 @@ internal enum XcodeBuild {
- returns: `xcodebuild`'s STDOUT output and, optionally, both STDERR+STDOUT output combined.
*/
internal static func launch(arguments: [String], inPath path: String, pipingStandardError: Bool = true) -> Data {
let task = Process()
let pathOfXcodebuild = "/usr/bin/xcodebuild"
task.arguments = arguments

let pipe = Pipe()
task.standardOutput = pipe

if pipingStandardError {
task.standardError = pipe
}

do {
#if canImport(Darwin)
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: pathOfXcodebuild)
task.currentDirectoryURL = URL(fileURLWithPath: path)
try task.run()
} else {
task.launchPath = pathOfXcodebuild
task.currentDirectoryPath = path
task.launch()
}
#elseif compiler(>=5)
task.executableURL = URL(fileURLWithPath: pathOfXcodebuild)
task.currentDirectoryURL = URL(fileURLWithPath: path)
try task.run()
#else
task.launchPath = pathOfXcodebuild
task.currentDirectoryPath = path
task.launch()
#endif
} catch {
return Data()
}

let file = pipe.fileHandleForReading
defer { file.closeFile() }

return file.readDataToEndOfFile()
return Exec.run("/usr/bin/xcodebuild",
arguments,
currentDirectory: path,
stderr: pipingStandardError ? .merge : .inherit).data
}

/**
Expand Down Expand Up @@ -233,37 +198,7 @@ public func sdkPath() -> String {
// xcrun does not exist on Linux
return ""
#else
let task = Process()
let pathOfXcrun = "/usr/bin/xcrun"
task.arguments = ["--show-sdk-path", "--sdk", "macosx"]

let pipe = Pipe()
task.standardOutput = pipe

do {
#if canImport(Darwin)
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: pathOfXcrun)
try task.run()
} else {
task.launchPath = pathOfXcrun
task.launch()
}
#elseif compiler(>=5)
task.executableURL = URL(fileURLWithPath: pathOfXcrun)
try task.run()
#else
task.launchPath = pathOfXcrun
task.launch()
#endif
} catch {
return ""
}

let file = pipe.fileHandleForReading
let sdkPath = String(data: file.readDataToEndOfFile(), encoding: .utf8)
file.closeFile()
return sdkPath?.replacingOccurrences(of: "\n", with: "") ?? ""
return Exec.run("/usr/bin/xcrun", "--show-sdk-path", "--sdk", "macosx").string ?? ""
#endif
}

Expand Down
61 changes: 4 additions & 57 deletions Source/SourceKittenFramework/library_wrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,51 +60,16 @@ private extension String {

// MARK: - Linux

/// Run a process at the given (absolute) path, capture output, return outupt.
private func runCommand(_ path: String, _ args: String...) -> String? {
let process = Process()
process.arguments = args

let pipe = Pipe()
process.standardOutput = pipe
// FileHandle.nullDevice does not work here, as it consists of an invalid file descriptor,
// causing process.launch() to abort with an EBADF.
process.standardError = FileHandle(forWritingAtPath: "/dev/null")!
do {
#if compiler(>=5)
process.executableURL = URL(fileURLWithPath: path)
try process.run()
#else
process.launchPath = path
process.launch()
#endif
} catch {
return nil
}

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 {
guard let swiftenvPath = Exec.run("/usr/bin/which", "swiftenv").string else {
return nil
}

guard let swiftenvRoot = runCommand(swiftenvPath, "prefix") else {
guard let swiftenvRoot = Exec.run(swiftenvPath, "prefix").string else {
return nil
}

Expand All @@ -114,7 +79,7 @@ internal let linuxFindSwiftenvActiveLibPath: String? = {
/// 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 {
guard let swiftPath = Exec.run("/usr/bin/which", "swift").string else {
return nil
}

Expand Down Expand Up @@ -189,25 +154,7 @@ private let xcrunFindPath: String? = {
return nil
}

let task = Process()
task.arguments = ["-find", "swift"]

let pipe = Pipe()
task.standardOutput = pipe
do {
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: pathOfXcrun)
try task.run()
} else {
task.launchPath = pathOfXcrun
task.launch() // if xcode-select does not exist, crash with `NSInvalidArgumentException`.
}
} catch {
return nil
}

let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8) else {
guard let output = Exec.run(pathOfXcrun, "-find", "swift").string else {
return nil
}

Expand Down
Loading

0 comments on commit 7e35031

Please sign in to comment.