Skip to content

Add templates for build tool plugins and command plugins #6111

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

Merged
merged 10 commits into from
Mar 29, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Note: This is in reverse chronological order, so newer entries are added to the
Swift Next
-----------

* [#6111]

Package creation using `package init` now also supports the build tool plugin and command plugin types.

* [#5728]

In packages that specify resources using a future tools version, the generated resource bundle accessor will import `Foundation.Bundle` for its own implementation only. _Clients_ of such packages therefore no longer silently import `Foundation`, preventing inadvertent use of Foundation extensions to standard library APIs, which helps to avoid unexpected code size increases.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Commands/PackageTools/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ extension SwiftPackageTool {
tool - A package with an executable that uses
Swift Argument Parser. Use this template if you
plan to have a rich set of command-line arguments.
build-tool-plugin - A package that vends a build tool plugin.
command-plugin - A package that vends a command plugin.
macro - A package that vends a macro.
empty - An empty package with a Package.swift manifest.
"""))
Expand Down
114 changes: 108 additions & 6 deletions Sources/Workspace/InitPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2020 Apple Inc. and the Swift project authors
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -44,7 +44,8 @@ public final class InitPackage {
case library = "library"
case executable = "executable"
case tool = "tool"
case `extension` = "extension"
case buildToolPlugin = "build-tool-plugin"
case commandPlugin = "command-plugin"
case macro = "macro"

public var description: String {
Expand Down Expand Up @@ -115,6 +116,7 @@ public final class InitPackage {
// none of it exists, and then act.
try writeManifestFile()
try writeGitIgnore()
try writePlugins()
try writeSources()
try writeTests()
}
Expand Down Expand Up @@ -146,6 +148,7 @@ public final class InitPackage {
}

stream <<< """

let package = Package(

"""
Expand Down Expand Up @@ -213,6 +216,15 @@ public final class InitPackage {
targets: ["\(pkgname)"]),
]
""")
} else if packageType == .buildToolPlugin || packageType == .commandPlugin {
pkgParams.append("""
products: [
// Products can be used to vend plugins, making them visible to other packages.
.plugin(
name: "\(pkgname)",
targets: ["\(pkgname)"]),
]
""")
} else if packageType == .macro {
pkgParams.append("""
products: [
Expand Down Expand Up @@ -269,6 +281,25 @@ public final class InitPackage {
]),
]
"""
} else if packageType == .buildToolPlugin {
param += """
.plugin(
name: "\(typeName)",
capability: .buildTool()
),
]
"""
} else if packageType == .commandPlugin {
param += """
.plugin(
name: "\(typeName)",
capability: .command(intent: .custom(
verb: "\(typeName)",
description: "prints hello world"
))
),
]
"""
} else if packageType == .macro {
param += """
// Macro implementation, only built for the host and never part of a client program.
Expand Down Expand Up @@ -350,8 +381,79 @@ public final class InitPackage {
}
}

private func writePlugins() throws {
switch packageType {
case .buildToolPlugin, .commandPlugin:
let plugins = destinationPath.appending(component: "Plugins")
guard self.fileSystem.exists(plugins) == false else {
return
}
progressReporter?("Creating \(plugins.relative(to: destinationPath))/")
try makeDirectories(plugins)

let moduleDir = plugins
try makeDirectories(moduleDir)

let sourceFileName = "\(pkgname).swift"
let sourceFile = try AbsolutePath(validating: sourceFileName, relativeTo: moduleDir)

var content = """
import PackagePlugin

@main

"""
if packageType == .buildToolPlugin {
content += """
struct \(typeName): BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
// The plugin can choose what parts of the package to process.
guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] }

// Find the code generator tool to run (replace this with the actual one).
let generatorTool = try context.tool(named: "my-code-generator")

// Construct a build command for each source file with a particular suffix.
return sourceFiles.map(\\.path).compactMap { inputPath in
guard inputPath.extension == "my-input-suffix" else { return .none }
let inputName = inputPath.lastComponent
let outputName = inputPath.stem + ".swift"
let outputPath = context.pluginWorkDirectory.appending(outputName)
return .buildCommand(
displayName: "Generating \\(outputName) from \\(inputName)",
executable: generatorTool.path,
arguments: ["\\(inputPath)", "-o", "\\(outputPath)"],
inputFiles: [inputPath],
outputFiles: [outputPath]
)
}
}
}

"""
}
else {
content += """
struct \(typeName): CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
print("Hello, World!")
}
}

"""
}

try writePackageFile(sourceFile) { stream in
stream.write(content)
}

case .empty, .library, .executable, .tool, .macro:
break
}
}

private func writeSources() throws {
if packageType == .empty {
if packageType == .empty || packageType == .buildToolPlugin || packageType == .commandPlugin {
return
}

Expand Down Expand Up @@ -427,7 +529,7 @@ public final class InitPackage {
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "\(moduleName)Macros", type: "StringifyMacro")
"""

case .empty, .`extension`:
case .empty, .buildToolPlugin, .commandPlugin:
throw InternalError("invalid packageType \(packageType)")
}

Expand All @@ -443,7 +545,7 @@ public final class InitPackage {

private func writeTests() throws {
switch packageType {
case .empty, .executable, .tool, .`extension`: return
case .empty, .executable, .tool, .buildToolPlugin, .commandPlugin: return
default: break
}
let tests = destinationPath.appending("Tests")
Expand Down Expand Up @@ -589,7 +691,7 @@ public final class InitPackage {

let testClassFile = try AbsolutePath(validating: "\(moduleName)Tests.swift", relativeTo: testModule)
switch packageType {
case .empty, .`extension`, .executable, .tool: break
case .empty, .buildToolPlugin, .commandPlugin, .executable, .tool: break
case .library:
try writeLibraryTestsFile(testClassFile)
case .macro:
Expand Down
61 changes: 60 additions & 1 deletion Tests/WorkspaceTests/InitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2017 Apple Inc. and the Swift project authors
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -149,6 +149,65 @@ class InitTests: XCTestCase {
}
}

func testInitPackageCommandPlugin() throws {
try testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("MyCommandPlugin")
let name = path.basename
try fs.createDirectory(path)

// Create the package
try InitPackage(
name: name,
packageType: .commandPlugin,
destinationPath: path,
fileSystem: localFileSystem
).writePackageStructure()

// Verify basic file system content that we expect in the package
let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let manifestContents: String = try localFileSystem.readFileContents(manifest)
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("targets: [\"MyCommandPlugin\"]")))
XCTAssertMatch(manifestContents, .and(.contains(".plugin("),
.and(.contains("capability: .command(intent: .custom("), .contains("verb: \"MyCommandPlugin\""))))

let source = path.appending("Plugins", "MyCommandPlugin.swift")
XCTAssertFileExists(source)
let sourceContents: String = try localFileSystem.readFileContents(source)
XCTAssertMatch(sourceContents, .contains("struct MyCommandPlugin: CommandPlugin"))
}
}

func testInitPackageBuildToolPlugin() throws {
try testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("MyBuildToolPlugin")
let name = path.basename
try fs.createDirectory(path)

// Create the package
try InitPackage(
name: name,
packageType: .buildToolPlugin,
destinationPath: path,
fileSystem: localFileSystem
).writePackageStructure()

// Verify basic file system content that we expect in the package
let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let manifestContents: String = try localFileSystem.readFileContents(manifest)
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("targets: [\"MyBuildToolPlugin\"]")))
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("capability: .buildTool()")))

let source = path.appending("Plugins", "MyBuildToolPlugin.swift")
XCTAssertFileExists(source)
let sourceContents: String = try localFileSystem.readFileContents(source)
XCTAssertMatch(sourceContents, .contains("struct MyBuildToolPlugin: BuildToolPlugin"))
}
}

// MARK: Special case testing

func testInitPackageNonc99Directory() throws {
Expand Down