forked from apple/swift-argument-parser
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental manual page generation (apple#332)
- Adds a swift package manager command plugin called GenerateManualPlugin. The plugin can be invoked from the command line using `swift package experimental-generate-manual`. The plugin is prefixed for now with "experimental-" to indicate it is not mature and may see breaking changes to its CLI and output in the future. The plugin can be can be used to generate a manual in MDoc syntax for any swift-argument-parser tool that can be executed via `tool --experimental-dump-info`. - The plugin works by converting the `ToolInfoV0` structure from the `ArgumentParserToolInfo` library into MDoc AST nodes using a custom (SwiftUI-esk) result builder DSL. The MDoc AST is then lowered to a string and written to disk. - The MDoc AST included is not general purpose and doesn't represent the true language exactly, so it is private to the underlying `generate-manual` tool. In the future it would be interesting to finish fleshing out this MDoc library and spin it out, however this is not a priority. - Next steps include: - Improving the command line interface for the plugin. - Adding support for "extended discussions" to Commands and exposing this information in manuals. - Further improve the escaping logic to properly escape MDoc macros that might happen to appear in user's help strings. - Ingesting external content a-la swift-docc so the entire tool documentation does not need to be included in the binary itself. - Bug fixes and addressing developer/user feedback. Built with love, @rauhul
- Loading branch information
Showing
39 changed files
with
3,771 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// swift-tools-version:5.6 | ||
//===----------------------------------------------------------*- swift -*-===// | ||
// | ||
// This source file is part of the Swift Argument Parser open source project | ||
// | ||
// Copyright (c) 2020 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import PackageDescription | ||
|
||
var package = Package( | ||
name: "swift-argument-parser", | ||
products: [ | ||
.library( | ||
name: "ArgumentParser", | ||
targets: ["ArgumentParser"]), | ||
], | ||
dependencies: [], | ||
targets: [ | ||
// Core Library | ||
.target( | ||
name: "ArgumentParser", | ||
dependencies: ["ArgumentParserToolInfo"], | ||
exclude: ["CMakeLists.txt"]), | ||
.target( | ||
name: "ArgumentParserTestHelpers", | ||
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], | ||
exclude: ["CMakeLists.txt"]), | ||
.target( | ||
name: "ArgumentParserToolInfo", | ||
dependencies: [ ], | ||
exclude: ["CMakeLists.txt"]), | ||
|
||
// Plugins | ||
.plugin( | ||
name: "GenerateManualPlugin", | ||
capability: .command( | ||
intent: .custom( | ||
verb: "experimental-generate-manual", | ||
description: "Generate a manual entry for a specified target.")), | ||
dependencies: ["generate-manual"]), | ||
|
||
// Examples | ||
.executableTarget( | ||
name: "roll", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/roll"), | ||
.executableTarget( | ||
name: "math", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/math"), | ||
.executableTarget( | ||
name: "repeat", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/repeat"), | ||
|
||
// Tools | ||
.executableTarget( | ||
name: "generate-manual", | ||
dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], | ||
path: "Tools/generate-manual"), | ||
|
||
// Tests | ||
.testTarget( | ||
name: "ArgumentParserEndToEndTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
.testTarget( | ||
name: "ArgumentParserExampleTests", | ||
dependencies: ["ArgumentParserTestHelpers"], | ||
resources: [.copy("CountLinesTest.txt")]), | ||
.testTarget( | ||
name: "ArgumentParserGenerateManualTests", | ||
dependencies: ["ArgumentParserTestHelpers"]), | ||
.testTarget( | ||
name: "ArgumentParserPackageManagerTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
.testTarget( | ||
name: "ArgumentParserUnitTests", | ||
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], | ||
exclude: ["CMakeLists.txt"]), | ||
] | ||
) | ||
|
||
#if os(macOS) | ||
package.targets.append(contentsOf: [ | ||
// Examples | ||
.executableTarget( | ||
name: "count-lines", | ||
dependencies: ["ArgumentParser"], | ||
path: "Examples/count-lines"), | ||
|
||
// Tools | ||
.executableTarget( | ||
name: "changelog-authors", | ||
dependencies: ["ArgumentParser"], | ||
path: "Tools/changelog-authors"), | ||
]) | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
//===----------------------------------------------------------*- swift -*-===// | ||
// | ||
// This source file is part of the Swift Argument Parser open source project | ||
// | ||
// Copyright (c) 2021 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import PackagePlugin | ||
import Foundation | ||
|
||
@main | ||
struct GenerateManualPlugin: CommandPlugin { | ||
func performCommand( | ||
context: PluginContext, | ||
arguments: [String] | ||
) async throws { | ||
// Locate generation tool. | ||
let generationToolFile = try context.tool(named: "generate-manual").path | ||
|
||
// Create an extractor to extract plugin-only arguments from the `arguments` | ||
// array. | ||
var extractor = ArgumentExtractor(arguments) | ||
|
||
// Run generation tool once if help is requested. | ||
if extractor.helpRequest() { | ||
try generationToolFile.exec(arguments: ["--help"]) | ||
print(""" | ||
ADDITIONAL OPTIONS: | ||
--configuration <configuration> | ||
Tool build configuration used to generate the | ||
manual. (default: release) | ||
NOTE: The "GenerateManual" plugin handles passing the "<tool>" and | ||
"--output-directory <output-directory>" arguments. Manually supplying | ||
these arguments will result in a runtime failure. | ||
""") | ||
return | ||
} | ||
|
||
// Extract configuration argument before making it to the | ||
// "generate-manual" tool. | ||
let configuration = try extractor.configuration() | ||
|
||
// Build all products first. | ||
print("Building package in \(configuration) mode...") | ||
let buildResult = try packageManager.build( | ||
.all(includingTests: false), | ||
parameters: .init(configuration: configuration)) | ||
|
||
guard buildResult.succeeded else { | ||
throw GenerateManualPluginError.buildFailed(buildResult.logText) | ||
} | ||
print("Built package in \(configuration) mode") | ||
|
||
// Run generate-manual on all executable artifacts. | ||
for builtArtifact in buildResult.builtArtifacts { | ||
// Skip non-executable targets | ||
guard builtArtifact.kind == .executable else { continue } | ||
|
||
// Skip executables without a matching product. | ||
guard let product = builtArtifact.matchingProduct(context: context) | ||
else { continue } | ||
|
||
// Skip products without a dependency on ArgumentParser. | ||
guard product.hasDependency(named: "ArgumentParser") else { continue } | ||
|
||
// Get the artifacts name. | ||
let executableName = builtArtifact.path.lastComponent | ||
print("Generating manual for \(executableName)...") | ||
|
||
// Create output directory. | ||
let outputDirectory = context | ||
.pluginWorkDirectory | ||
.appending(executableName) | ||
try outputDirectory.createOutputDirectory() | ||
|
||
// Create generation tool arguments. | ||
var generationToolArguments = [ | ||
builtArtifact.path.string, | ||
"--output-directory", | ||
outputDirectory.string | ||
] | ||
generationToolArguments.append( | ||
contentsOf: extractor.unextractedOptionsOrFlags) | ||
|
||
// Spawn generation tool. | ||
try generationToolFile.exec(arguments: generationToolArguments) | ||
print("Generated manual in '\(outputDirectory)'") | ||
} | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
Plugins/GenerateManualPlugin/GenerateManualPluginError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
//===----------------------------------------------------------*- swift -*-===// | ||
// | ||
// This source file is part of the Swift Argument Parser open source project | ||
// | ||
// Copyright (c) 2021 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Foundation | ||
import PackagePlugin | ||
|
||
enum GenerateManualPluginError: Error { | ||
case unknownBuildConfiguration(String) | ||
case buildFailed(String) | ||
case createOutputDirectoryFailed(Error) | ||
case subprocessFailedNonZeroExit(Path, Int32) | ||
case subprocessFailedError(Path, Error) | ||
} | ||
|
||
extension GenerateManualPluginError: CustomStringConvertible { | ||
var description: String { | ||
switch self { | ||
case .unknownBuildConfiguration(let configuration): | ||
return "Build failed: Unknown build configuration '\(configuration)'." | ||
case .buildFailed(let logText): | ||
return "Build failed: \(logText)." | ||
case .createOutputDirectoryFailed(let error): | ||
return """ | ||
Failed to create output directory: '\(error.localizedDescription)' | ||
""" | ||
case .subprocessFailedNonZeroExit(let tool, let exitCode): | ||
return """ | ||
'\(tool.lastComponent)' invocation failed with a nonzero exit code: \ | ||
'\(exitCode)'. | ||
""" | ||
case .subprocessFailedError(let tool, let error): | ||
return """ | ||
'\(tool.lastComponent)' invocation failed: \ | ||
'\(error.localizedDescription)' | ||
""" | ||
} | ||
} | ||
} | ||
|
||
extension GenerateManualPluginError: LocalizedError { | ||
var localizedDescription: String { self.description } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
//===----------------------------------------------------------*- swift -*-===// | ||
// | ||
// This source file is part of the Swift Argument Parser open source project | ||
// | ||
// Copyright (c) 2021 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Foundation | ||
import PackagePlugin | ||
|
||
extension ArgumentExtractor { | ||
mutating func helpRequest() -> Bool { | ||
self.extractFlag(named: "help") > 0 | ||
} | ||
|
||
mutating func configuration() throws -> PackageManager.BuildConfiguration { | ||
switch self.extractOption(named: "configuration").first { | ||
case .some(let configurationString): | ||
switch configurationString { | ||
case "debug": | ||
return .debug | ||
case "release": | ||
return .release | ||
default: | ||
throw GenerateManualPluginError | ||
.unknownBuildConfiguration(configurationString) | ||
} | ||
case .none: | ||
return .release | ||
} | ||
} | ||
} | ||
|
||
extension Path { | ||
func createOutputDirectory() throws { | ||
do { | ||
try FileManager.default.createDirectory( | ||
atPath: self.string, | ||
withIntermediateDirectories: true) | ||
} catch { | ||
throw GenerateManualPluginError.createOutputDirectoryFailed(error) | ||
} | ||
} | ||
|
||
func exec(arguments: [String]) throws { | ||
do { | ||
let process = Process() | ||
process.executableURL = URL(fileURLWithPath: self.string) | ||
process.arguments = arguments | ||
try process.run() | ||
process.waitUntilExit() | ||
guard | ||
process.terminationReason == .exit, | ||
process.terminationStatus == 0 | ||
else { | ||
throw GenerateManualPluginError.subprocessFailedNonZeroExit( | ||
self, process.terminationStatus) | ||
} | ||
} catch { | ||
throw GenerateManualPluginError.subprocessFailedError(self, error) | ||
} | ||
} | ||
} | ||
|
||
extension PackageManager.BuildResult.BuiltArtifact { | ||
func matchingProduct(context: PluginContext) -> Product? { | ||
context | ||
.package | ||
.products | ||
.first { $0.name == self.path.lastComponent } | ||
} | ||
} | ||
|
||
extension Product { | ||
func hasDependency(named name: String) -> Bool { | ||
recursiveTargetDependencies | ||
.contains { $0.name == name } | ||
} | ||
|
||
var recursiveTargetDependencies: [Target] { | ||
var dependencies = [Target.ID: Target]() | ||
for target in self.targets { | ||
for dependency in target.recursiveTargetDependencies { | ||
dependencies[dependency.id] = dependency | ||
} | ||
} | ||
return Array(dependencies.values) | ||
} | ||
} |
Oops, something went wrong.