Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental manual page generation #332

Merged
merged 3 commits into from Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions Package.swift
Expand Up @@ -21,6 +21,7 @@ var package = Package(
],
dependencies: [],
targets: [
// Core Library
.target(
name: "ArgumentParser",
dependencies: ["ArgumentParserToolInfo"],
Expand All @@ -34,6 +35,7 @@ var package = Package(
dependencies: [],
exclude: ["CMakeLists.txt"]),

// Examples
.executableTarget(
name: "roll",
dependencies: ["ArgumentParser"],
Expand All @@ -47,6 +49,7 @@ var package = Package(
dependencies: ["ArgumentParser"],
path: "Examples/repeat"),

// Tests
.testTarget(
name: "ArgumentParserEndToEndTests",
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
Expand All @@ -68,10 +71,13 @@ var package = Package(

#if swift(>=5.6) && os(macOS)
package.targets.append(contentsOf: [
// Examples
.executableTarget(
name: "count-lines",
dependencies: ["ArgumentParser"],
path: "Examples/count-lines"),

// Tools
.executableTarget(
name: "changelog-authors",
dependencies: ["ArgumentParser"],
Expand Down
104 changes: 104 additions & 0 deletions Package@swift-5.6.swift
@@ -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
95 changes: 95 additions & 0 deletions Plugins/GenerateManualPlugin/GenerateManualPlugin.swift
@@ -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 Plugins/GenerateManualPlugin/GenerateManualPluginError.swift
@@ -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 }
}
93 changes: 93 additions & 0 deletions Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift
@@ -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)
}
}