Skip to content

Commit 6c4326a

Browse files
abertelrudneonichu
andauthored
[5.9] Add templates for build tool plugins and command plugins (#6352)
Adds templates for build tool plugins and command plugins, allowing them to be created from the `swift package init` command. The packages are set up to vend the plugins via plugin product declarations, so they can be used by other packages. The initial code for each each plugin type is minimal, but for the build tool plugin type it shows how to create and return a build command for each source file of a particular type, since that's the most common use of build tool plugins. rdar://107288352 Co-authored-by: Anders Bertelrud <anders@apple.com> (cherry picked from commit f25e138) Co-authored-by: Boris Bügling <bbuegling@apple.com>
1 parent 77e5716 commit 6c4326a

File tree

4 files changed

+174
-7
lines changed

4 files changed

+174
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Note: This is in reverse chronological order, so newer entries are added to the
33
Swift Next
44
-----------
55

6+
* [#6111]
7+
8+
Package creation using `package init` now also supports the build tool plugin and command plugin types.
9+
610
* [#5728]
711

812
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.

Sources/Commands/PackageTools/Init.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ extension SwiftPackageTool {
3131
tool - A package with an executable that uses
3232
Swift Argument Parser. Use this template if you
3333
plan to have a rich set of command-line arguments.
34+
build-tool-plugin - A package that vends a build tool plugin.
35+
command-plugin - A package that vends a command plugin.
3436
macro - A package that vends a macro.
3537
empty - An empty package with a Package.swift manifest.
3638
"""))

Sources/Workspace/InitPackage.swift

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2014-2020 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -44,7 +44,8 @@ public final class InitPackage {
4444
case library = "library"
4545
case executable = "executable"
4646
case tool = "tool"
47-
case `extension` = "extension"
47+
case buildToolPlugin = "build-tool-plugin"
48+
case commandPlugin = "command-plugin"
4849
case macro = "macro"
4950

5051
public var description: String {
@@ -115,6 +116,7 @@ public final class InitPackage {
115116
// none of it exists, and then act.
116117
try writeManifestFile()
117118
try writeGitIgnore()
119+
try writePlugins()
118120
try writeSources()
119121
try writeTests()
120122
}
@@ -146,6 +148,7 @@ public final class InitPackage {
146148
}
147149

148150
stream <<< """
151+
149152
let package = Package(
150153
151154
"""
@@ -213,6 +216,15 @@ public final class InitPackage {
213216
targets: ["\(pkgname)"]),
214217
]
215218
""")
219+
} else if packageType == .buildToolPlugin || packageType == .commandPlugin {
220+
pkgParams.append("""
221+
products: [
222+
// Products can be used to vend plugins, making them visible to other packages.
223+
.plugin(
224+
name: "\(pkgname)",
225+
targets: ["\(pkgname)"]),
226+
]
227+
""")
216228
} else if packageType == .macro {
217229
pkgParams.append("""
218230
products: [
@@ -269,6 +281,25 @@ public final class InitPackage {
269281
]),
270282
]
271283
"""
284+
} else if packageType == .buildToolPlugin {
285+
param += """
286+
.plugin(
287+
name: "\(typeName)",
288+
capability: .buildTool()
289+
),
290+
]
291+
"""
292+
} else if packageType == .commandPlugin {
293+
param += """
294+
.plugin(
295+
name: "\(typeName)",
296+
capability: .command(intent: .custom(
297+
verb: "\(typeName)",
298+
description: "prints hello world"
299+
))
300+
),
301+
]
302+
"""
272303
} else if packageType == .macro {
273304
param += """
274305
// Macro implementation, only built for the host and never part of a client program.
@@ -350,8 +381,79 @@ public final class InitPackage {
350381
}
351382
}
352383

384+
private func writePlugins() throws {
385+
switch packageType {
386+
case .buildToolPlugin, .commandPlugin:
387+
let plugins = destinationPath.appending(component: "Plugins")
388+
guard self.fileSystem.exists(plugins) == false else {
389+
return
390+
}
391+
progressReporter?("Creating \(plugins.relative(to: destinationPath))/")
392+
try makeDirectories(plugins)
393+
394+
let moduleDir = plugins
395+
try makeDirectories(moduleDir)
396+
397+
let sourceFileName = "\(pkgname).swift"
398+
let sourceFile = try AbsolutePath(validating: sourceFileName, relativeTo: moduleDir)
399+
400+
var content = """
401+
import PackagePlugin
402+
403+
@main
404+
405+
"""
406+
if packageType == .buildToolPlugin {
407+
content += """
408+
struct \(typeName): BuildToolPlugin {
409+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
410+
// The plugin can choose what parts of the package to process.
411+
guard let sourceFiles = target.sourceModule?.sourceFiles else { return [] }
412+
413+
// Find the code generator tool to run (replace this with the actual one).
414+
let generatorTool = try context.tool(named: "my-code-generator")
415+
416+
// Construct a build command for each source file with a particular suffix.
417+
return sourceFiles.map(\\.path).compactMap { inputPath in
418+
guard inputPath.extension == "my-input-suffix" else { return .none }
419+
let inputName = inputPath.lastComponent
420+
let outputName = inputPath.stem + ".swift"
421+
let outputPath = context.pluginWorkDirectory.appending(outputName)
422+
return .buildCommand(
423+
displayName: "Generating \\(outputName) from \\(inputName)",
424+
executable: generatorTool.path,
425+
arguments: ["\\(inputPath)", "-o", "\\(outputPath)"],
426+
inputFiles: [inputPath],
427+
outputFiles: [outputPath]
428+
)
429+
}
430+
}
431+
}
432+
433+
"""
434+
}
435+
else {
436+
content += """
437+
struct \(typeName): CommandPlugin {
438+
func performCommand(context: PluginContext, arguments: [String]) async throws {
439+
print("Hello, World!")
440+
}
441+
}
442+
443+
"""
444+
}
445+
446+
try writePackageFile(sourceFile) { stream in
447+
stream.write(content)
448+
}
449+
450+
case .empty, .library, .executable, .tool, .macro:
451+
break
452+
}
453+
}
454+
353455
private func writeSources() throws {
354-
if packageType == .empty {
456+
if packageType == .empty || packageType == .buildToolPlugin || packageType == .commandPlugin {
355457
return
356458
}
357459

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

430-
case .empty, .`extension`:
532+
case .empty, .buildToolPlugin, .commandPlugin:
431533
throw InternalError("invalid packageType \(packageType)")
432534
}
433535

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

444546
private func writeTests() throws {
445547
switch packageType {
446-
case .empty, .executable, .tool, .`extension`: return
548+
case .empty, .executable, .tool, .buildToolPlugin, .commandPlugin: return
447549
default: break
448550
}
449551
let tests = destinationPath.appending("Tests")
@@ -589,7 +691,7 @@ public final class InitPackage {
589691

590692
let testClassFile = try AbsolutePath(validating: "\(moduleName)Tests.swift", relativeTo: testModule)
591693
switch packageType {
592-
case .empty, .`extension`, .executable, .tool: break
694+
case .empty, .buildToolPlugin, .commandPlugin, .executable, .tool: break
593695
case .library:
594696
try writeLibraryTestsFile(testClassFile)
595697
case .macro:

Tests/WorkspaceTests/InitTests.swift

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2014-2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -149,6 +149,65 @@ class InitTests: XCTestCase {
149149
}
150150
}
151151

152+
func testInitPackageCommandPlugin() throws {
153+
try testWithTemporaryDirectory { tmpPath in
154+
let fs = localFileSystem
155+
let path = tmpPath.appending("MyCommandPlugin")
156+
let name = path.basename
157+
try fs.createDirectory(path)
158+
159+
// Create the package
160+
try InitPackage(
161+
name: name,
162+
packageType: .commandPlugin,
163+
destinationPath: path,
164+
fileSystem: localFileSystem
165+
).writePackageStructure()
166+
167+
// Verify basic file system content that we expect in the package
168+
let manifest = path.appending("Package.swift")
169+
XCTAssertFileExists(manifest)
170+
let manifestContents: String = try localFileSystem.readFileContents(manifest)
171+
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("targets: [\"MyCommandPlugin\"]")))
172+
XCTAssertMatch(manifestContents, .and(.contains(".plugin("),
173+
.and(.contains("capability: .command(intent: .custom("), .contains("verb: \"MyCommandPlugin\""))))
174+
175+
let source = path.appending("Plugins", "MyCommandPlugin.swift")
176+
XCTAssertFileExists(source)
177+
let sourceContents: String = try localFileSystem.readFileContents(source)
178+
XCTAssertMatch(sourceContents, .contains("struct MyCommandPlugin: CommandPlugin"))
179+
}
180+
}
181+
182+
func testInitPackageBuildToolPlugin() throws {
183+
try testWithTemporaryDirectory { tmpPath in
184+
let fs = localFileSystem
185+
let path = tmpPath.appending("MyBuildToolPlugin")
186+
let name = path.basename
187+
try fs.createDirectory(path)
188+
189+
// Create the package
190+
try InitPackage(
191+
name: name,
192+
packageType: .buildToolPlugin,
193+
destinationPath: path,
194+
fileSystem: localFileSystem
195+
).writePackageStructure()
196+
197+
// Verify basic file system content that we expect in the package
198+
let manifest = path.appending("Package.swift")
199+
XCTAssertFileExists(manifest)
200+
let manifestContents: String = try localFileSystem.readFileContents(manifest)
201+
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("targets: [\"MyBuildToolPlugin\"]")))
202+
XCTAssertMatch(manifestContents, .and(.contains(".plugin("), .contains("capability: .buildTool()")))
203+
204+
let source = path.appending("Plugins", "MyBuildToolPlugin.swift")
205+
XCTAssertFileExists(source)
206+
let sourceContents: String = try localFileSystem.readFileContents(source)
207+
XCTAssertMatch(sourceContents, .contains("struct MyBuildToolPlugin: BuildToolPlugin"))
208+
}
209+
}
210+
152211
// MARK: Special case testing
153212

154213
func testInitPackageNonc99Directory() throws {

0 commit comments

Comments
 (0)