Skip to content

Commit 28baaeb

Browse files
committed
Run prebuild commands from plugins before the build starts
The generated source file paths are also added to the build plan llbuild task so that the build plan is recomputed if there are any changes to them. rdar://74614476
1 parent f73c0c7 commit 28baaeb

File tree

18 files changed

+155
-45
lines changed

18 files changed

+155
-45
lines changed

Fixtures/Miscellaneous/Plugins/MySourceGenClient/Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ let package = Package(
77
.package(path: "../MySourceGenPlugin")
88
],
99
targets: [
10-
// A tool that uses an plugin.
10+
// A tool that uses a plugin.
1111
.executableTarget(
1212
name: "MyTool",
1313
dependencies: [
14-
.product(name: "MySourceGenPlugin", package: "MySourceGenPlugin")
14+
.product(name: "MySourceGenBuildToolPlugin", package: "MySourceGenPlugin")
1515
]
1616
),
17-
// A unit that uses the plugin.
17+
// A unit test that uses the plugin.
1818
.testTarget(
1919
name: "MyTests",
2020
plugins: [
21-
.plugin(name: "MySourceGenPlugin", package: "MySourceGenPlugin")
21+
.plugin(name: "MySourceGenBuildToolPlugin", package: "MySourceGenPlugin")
2222
]
2323
)
2424
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Hello Extension Product!
1+
I am Foo!
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
print("Generated string: '\(generatedString)'")
1+
print("Generated string Foo: '\(foo)'")

Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Package.swift

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,60 @@ import PackageDescription
44
let package = Package(
55
name: "MySourceGenPlugin",
66
products: [
7-
// The product that vends MySourceGenPlugin to client packages.
7+
// The product that vends MySourceGenBuildToolPlugin to client packages.
88
.plugin(
9-
name: "MySourceGenPlugin",
10-
targets: ["MySourceGenPlugin"]
9+
name: "MySourceGenBuildToolPlugin",
10+
targets: ["MySourceGenBuildToolPlugin"]
1111
),
12+
// The product that vends the MySourceGenBuildTool executable to client packages.
1213
.executable(
13-
name: "MySourceGenTool",
14-
targets: ["MySourceGenTool"]
15-
)
14+
name: "MySourceGenBuildTool",
15+
targets: ["MySourceGenBuildTool"]
16+
),
17+
// The product that vends MySourceGenPrebuildPlugin to client packages.
18+
.plugin(
19+
name: "MySourceGenPrebuildPlugin",
20+
targets: ["MySourceGenPrebuildPlugin"]
21+
),
1622
],
1723
targets: [
18-
// A local tool that uses a plugin.
24+
// A local tool that uses a build tool plugin.
1925
.executableTarget(
2026
name: "MyLocalTool",
2127
dependencies: [
22-
"MySourceGenPlugin",
28+
"MySourceGenBuildToolPlugin",
29+
]
30+
),
31+
// A local tool that uses a prebuild plugin.
32+
.executableTarget(
33+
name: "MyOtherLocalTool",
34+
dependencies: [
35+
"MySourceGenPrebuildPlugin",
2336
]
2437
),
25-
// The target that implements the plugin and generates commands to invoke MySourceGenTool.
38+
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
2639
.plugin(
27-
name: "MySourceGenPlugin",
40+
name: "MySourceGenBuildToolPlugin",
2841
capability: .buildTool(),
2942
dependencies: [
30-
"MySourceGenTool"
43+
"MySourceGenBuildTool",
3144
]
3245
),
46+
// The plugin that generates prebuild commands (currently to invoke a system tool).
47+
.plugin(
48+
name: "MySourceGenPrebuildPlugin",
49+
capability: .prebuild()
50+
),
3351
// The command line tool that generates source files.
3452
.executableTarget(
35-
name: "MySourceGenTool",
53+
name: "MySourceGenBuildTool",
3654
dependencies: [
37-
"MySourceGenToolLib",
55+
"MySourceGenBuildToolLib",
3856
]
3957
),
40-
// A library used by MySourceGenTool (not the client).
58+
// A library used by MySourceGenBuildTool (not the client).
4159
.target(
42-
name: "MySourceGenToolLib"
60+
name: "MySourceGenBuildToolLib"
4361
),
4462
// A runtime library that the client needs to link against.
4563
.target(
@@ -49,10 +67,11 @@ let package = Package(
4967
.testTarget(
5068
name: "MySourceGenPluginTests",
5169
dependencies: [
52-
"MySourceGenRuntimeLib"
70+
"MySourceGenRuntimeLib",
5371
],
5472
plugins: [
55-
"MySourceGenPlugin"
73+
"MySourceGenBuildToolPlugin",
74+
"MySourceGenPrebuildPlugin",
5675
]
5776
)
5877
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Hello Extension Target!
1+
I am Foo!
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
print("Generated string: '\(generatedString)'")
1+
print("Generated string Foo: '\(foo)'")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Bar!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Baz!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// print("Generated string Bar: '\(bar)'")
2+
// print("Generated string Baz: '\(baz)'")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
import MySourceGenBuildToolLib
3+
4+
// Sample source generator tool that emits a Swift variable declaration of a string containing the hex representation of the contents of a file as a quoted string. The variable name is the base name of the input file. The input file is the first argument and the output file is the second.
5+
if ProcessInfo.processInfo.arguments.count != 3 {
6+
print("usage: MySourceGenBuildTool <input> <output>")
7+
exit(1)
8+
}
9+
let inputFile = ProcessInfo.processInfo.arguments[1]
10+
let outputFile = ProcessInfo.processInfo.arguments[2]
11+
12+
let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent
13+
14+
let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
15+
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
16+
let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n"
17+
let outputData = outputString.data(using: .utf8)
18+
FileManager.default.createFile(atPath: outputFile, contents: outputData)

Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenPlugin/plugin.swift renamed to Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenBuildToolPlugin/plugin.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import PackagePlugin
2+
3+
print("Hello from the Build Tool Plugin!")
24

35
for inputPath in targetBuildContext.otherFiles {
46
guard inputPath.suffix == ".dat" else { continue }
@@ -8,7 +10,7 @@ for inputPath in targetBuildContext.otherFiles {
810
displayName:
911
"Generating \(outputName) from \(inputPath.filename)",
1012
executable:
11-
try targetBuildContext.lookupTool(named: "MySourceGenTool"),
13+
try targetBuildContext.lookupTool(named: "MySourceGenBuildTool"),
1214
arguments: [
1315
"\(inputPath)",
1416
"\(outputPath)"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import PackagePlugin
2+
3+
print("Hello from the Prebuild Plugin!")
4+
5+
let outputPaths: [Path] = targetBuildContext.otherFiles.filter{ $0.suffix == ".dat" }.map { path in
6+
targetBuildContext.outputDir.appending(path.basename + ".swift")
7+
}
8+
9+
if !outputPaths.isEmpty {
10+
commandConstructor.createCommand(
11+
displayName:
12+
"Running prebuild command for target \(targetBuildContext.targetName)",
13+
executable:
14+
Path("/usr/bin/touch"),
15+
arguments:
16+
outputPaths.map{ $0.string },
17+
derivedSourcePaths:
18+
outputPaths
19+
)
20+
}

Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenTool/main.swift

Lines changed: 0 additions & 16 deletions
This file was deleted.

Sources/Build/BuildOperation.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,53 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
163163
}
164164
}
165165

166+
/// Run any prebuild commands associated with the given list of prebuild results, and return the paths of any created files or directories.
167+
private func runPrebuildCommands(for results: [ExtensionEvaluationResult]) throws -> [AbsolutePath] {
168+
// Run through all the commands from all the extension usages in the target.
169+
var outputPaths: [AbsolutePath] = []
170+
for command in results.reduce([], { $0 + $1.commands }) {
171+
if case .prebuildCommand(let displayName, let executable, let arguments, _, _, let derivedSourceDirPaths) = command {
172+
// Run the prebuild command.
173+
// TODO: We need to also respect any environment and working directory configurations.
174+
let execPath = AbsolutePath(executable, relativeTo: buildParameters.buildPath)
175+
176+
// FIXME: Is this the right way to emit progress here?
177+
stdoutStream.write(displayName)
178+
179+
// Run the command as a subprocess.
180+
// TODO: This should move somewhere else, and we should also be able to run independent prebuild commands concurrently.
181+
let command = [execPath.pathString] + arguments
182+
let result = try Process.popen(arguments: command)
183+
let output = try (result.utf8Output() + result.utf8stderrOutput()).spm_chuzzle() ?? ""
184+
if result.exitStatus != .terminated(code: 0) {
185+
// TODO: Make this a proper error.
186+
throw StringError("failed: \(command)\n\n\(output)")
187+
}
188+
189+
// Keep track of the output paths.
190+
outputPaths.append(contentsOf: derivedSourceDirPaths)
191+
}
192+
}
193+
return outputPaths
194+
}
166195
/// Create the build plan and return the build description.
167196
private func plan() throws -> BuildDescription {
168197
let graph = try getPackageGraph()
169198
let pluginInvocationResults = try getPluginInvocationResults(for: graph)
199+
200+
// Run any prebuild commands. We get here in the case where there was no build description or it needs to change.
201+
var prebuildOutputPaths: [ResolvedTarget: [AbsolutePath]] = [:]
202+
for target in graph.reachableTargets {
203+
if let results = pluginInvocationResults[target] {
204+
prebuildOutputPaths[target] = try runPrebuildCommands(for: results)
205+
}
206+
}
207+
170208
let plan = try BuildPlan(
171209
buildParameters: buildParameters,
172210
graph: graph,
173211
pluginInvocationResults: pluginInvocationResults,
212+
prebuildOutputPaths: prebuildOutputPaths,
174213
diagnostics: diagnostics
175214
)
176215
self.buildPlan = plan

Sources/Build/BuildPlan.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,11 +544,15 @@ public final class SwiftTargetBuildDescription {
544544
/// The results of applying any plugins to this target.
545545
public let pluginInvocationResults: [PluginInvocationResult]
546546

547+
/// The output paths of any prebuild commands that were run for this target.
548+
public let prebuildOutputPaths: [AbsolutePath]
549+
547550
/// Create a new target description with target and build parameters.
548551
init(
549552
target: ResolvedTarget,
550553
buildParameters: BuildParameters,
551554
pluginInvocationResults: [PluginInvocationResult] = [],
555+
prebuildOutputPaths: [AbsolutePath] = [],
552556
isTestTarget: Bool? = nil,
553557
testDiscoveryTarget: Bool = false,
554558
fs: FileSystem = localFileSystem
@@ -563,7 +567,14 @@ public final class SwiftTargetBuildDescription {
563567
self.tempsPath = buildParameters.buildPath.appending(component: target.c99name + ".build")
564568
self.derivedSources = Sources(paths: [], root: tempsPath.appending(component: "DerivedSources"))
565569
self.pluginInvocationResults = pluginInvocationResults
570+
self.prebuildOutputPaths = prebuildOutputPaths
566571

572+
// Add any derived source paths from prebuild commands.
573+
for absPath in self.prebuildOutputPaths {
574+
let relPath = absPath.relative(to: self.derivedSources.root)
575+
self.derivedSources.relativePaths.append(relPath)
576+
}
577+
567578
// Add any derived source paths declared by build-tool plugins that were applied to this target. We do
568579
// this here and not just in the LLBuildManifestBuilder because we need to include them in any situation
569580
// where sources are processed, e.g. when determining names of object files, etc.
@@ -1278,6 +1289,9 @@ public class BuildPlan {
12781289
/// The results of invoking any plugins used by targets in this build.
12791290
public let pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]]
12801291

1292+
/// The output paths of any prebuild commands run for targets in this build.
1293+
public let prebuildOutputPaths: [ResolvedTarget: [AbsolutePath]]
1294+
12811295
/// The filesystem to operate on.
12821296
let fileSystem: FileSystem
12831297

@@ -1358,12 +1372,14 @@ public class BuildPlan {
13581372
buildParameters: BuildParameters,
13591373
graph: PackageGraph,
13601374
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
1375+
prebuildOutputPaths: [ResolvedTarget: [AbsolutePath]] = [:],
13611376
diagnostics: DiagnosticsEngine,
13621377
fileSystem: FileSystem = localFileSystem
13631378
) throws {
13641379
self.buildParameters = buildParameters
13651380
self.graph = graph
13661381
self.pluginInvocationResults = pluginInvocationResults
1382+
self.prebuildOutputPaths = prebuildOutputPaths
13671383
self.diagnostics = diagnostics
13681384
self.fileSystem = fileSystem
13691385

@@ -1389,6 +1405,7 @@ public class BuildPlan {
13891405
target: target,
13901406
buildParameters: buildParameters,
13911407
pluginInvocationResults: pluginInvocationResults[target] ?? [],
1408+
prebuildOutputPaths: prebuildOutputPaths[target] ?? [],
13921409
fs: fileSystem))
13931410
case is ClangTarget:
13941411
targetMap[target] = try .clang(ClangTargetBuildDescription(

Sources/Build/ManifestBuilder.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ extension LLBuildManifestBuilder {
108108
.sorted()
109109
.map { Node.directoryStructure($0) }
110110

111+
// Add the output paths of any prebuilds that were run, so that we redo the plan if they change.
112+
let nodes = plan.prebuildOutputPaths.values.flatMap{ $0 }.sorted().map { Node.file($0) }
113+
inputs.append(contentsOf: nodes)
114+
111115
// FIXME: Need to handle version-specific manifests.
112116
inputs.append(file: package.manifest.path)
113117

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,12 @@ final class PackageToolTests: XCTestCase {
302302
// Check the contents of the JSON.
303303
XCTAssertEqual(try XCTUnwrap(json["name"]).string, "MySourceGenPlugin")
304304
let targetsArray = try XCTUnwrap(json["targets"]?.array)
305-
let extensionTarget = try XCTUnwrap(targetsArray.first{ $0["name"]?.string == "MySourceGenPlugin" }?.dictionary)
306-
XCTAssertEqual(extensionTarget["module_type"]?.string, "PluginTarget")
307-
XCTAssertEqual(extensionTarget["plugin_capability"]?.dictionary?["type"]?.string, "buildTool")
305+
let buildToolExtTarget = try XCTUnwrap(targetsArray.first{ $0["name"]?.string == "MySourceGenBuildToolPlugin" }?.dictionary)
306+
XCTAssertEqual(buildToolExtTarget["module_type"]?.string, "PluginTarget")
307+
XCTAssertEqual(buildToolExtTarget["plugin_capability"]?.dictionary?["type"]?.string, "buildTool")
308+
let prebuildExtTarget = try XCTUnwrap(targetsArray.first{ $0["name"]?.string == "MySourceGenPrebuildPlugin" }?.dictionary)
309+
XCTAssertEqual(prebuildExtTarget["module_type"]?.string, "PluginTarget")
310+
XCTAssertEqual(prebuildExtTarget["plugin_capability"]?.dictionary?["type"]?.string, "prebuild")
308311
}
309312
}
310313

0 commit comments

Comments
 (0)