Skip to content

Commit 5e59c9f

Browse files
authored
Run prebuild commands from plugins before the build starts (#3307)
These commands run before the build starts, and can specify directories whose contents should be considered as inputs to the build planning. The contents of the directories are added as derived sources to the build, and are expected to be compatible with the kind of target for which they are generating sources (e.g. .swift files for Swift targets). The diagnostics for this are currently missing, resulting in a compilation failure rather than a better diagnostics, but will be added. Also, resource files should be treated as resources, etc — this tool will be added. The directories into which outputs are written are added to the set of directories whose contents should affect the build plan, so that changes to the set of generated source files are taken into account when constructing the build plan.
1 parent de1c3ba commit 5e59c9f

File tree

29 files changed

+348
-173
lines changed

29 files changed

+348
-173
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
]

Fixtures/Miscellaneous/Plugins/MySourceGenClient/Sources/MyTool/Foo.dat

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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
]

Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MyLocalTool/Foo.dat

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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)
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)"
@@ -18,9 +20,7 @@ for inputPath in targetBuildContext.otherFiles {
1820
],
1921
outputPaths: [
2022
outputPath
21-
],
22-
derivedSourcePaths: [
23-
outputPath
2423
]
2524
)
25+
commandConstructor.addGeneratedOutputFile(path: outputPath)
2626
}
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+
)
18+
}
19+
20+
commandConstructor.addPrebuildOutputDirectory(path: targetBuildContext.outputDir)

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

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

Sources/Build/BuildOperation.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,28 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
165165

166166
/// Create the build plan and return the build description.
167167
private func plan() throws -> BuildDescription {
168+
// Load the package graph.
168169
let graph = try getPackageGraph()
170+
171+
// Invoke any plugins in the graph, and get the results.
169172
let pluginInvocationResults = try getPluginInvocationResults(for: graph)
173+
174+
// Run any prebuild commands provided by plugins. Any failure stops the build.
175+
let prebuildCommandResults = try graph.reachableTargets.reduce(into: [:], { partial, target in
176+
partial[target] = try pluginInvocationResults[target].map { try runPrebuildCommands(for: $0) }
177+
})
178+
179+
// Create the build plan based, on the graph and any information from plugins.
170180
let plan = try BuildPlan(
171181
buildParameters: buildParameters,
172182
graph: graph,
173183
pluginInvocationResults: pluginInvocationResults,
184+
prebuildCommandResults: prebuildCommandResults,
174185
diagnostics: diagnostics
175186
)
176187
self.buildPlan = plan
177-
188+
189+
// Finally create the llbuild manifest from the plan.
178190
return try BuildDescription.create(with: plan)
179191
}
180192

Sources/Build/BuildPlan.swift

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -482,18 +482,23 @@ public final class SwiftTargetBuildDescription {
482482
///
483483
/// These are the source files generated during the build.
484484
private var derivedSources: Sources
485+
486+
/// These are the source files derived from plugins.
487+
private var pluginDerivedSources: Sources
485488

486489
/// Path to the bundle generated for this module (if any).
487490
var bundlePath: AbsolutePath? {
488491
buildParameters.bundlePath(for: target)
489492
}
490493

491494
/// The list of all source files in the target, including the derived ones.
492-
public var sources: [AbsolutePath] { target.sources.paths + derivedSources.paths }
495+
public var sources: [AbsolutePath] {
496+
target.sources.paths + derivedSources.paths + pluginDerivedSources.paths
497+
}
493498

494499
/// The objects in this target.
495500
public var objects: [AbsolutePath] {
496-
let relativePaths = target.sources.relativePaths + derivedSources.relativePaths
501+
let relativePaths = target.sources.relativePaths + derivedSources.relativePaths + pluginDerivedSources.relativePaths
497502
return relativePaths.map{ tempsPath.appending(RelativePath("\($0.pathString).o")) }
498503
}
499504

@@ -544,11 +549,15 @@ public final class SwiftTargetBuildDescription {
544549
/// The results of applying any plugins to this target.
545550
public let pluginInvocationResults: [PluginInvocationResult]
546551

552+
/// The results of running any prebuild commands for this target.
553+
public let prebuildCommandResults: [PrebuildCommandResult]
554+
547555
/// Create a new target description with target and build parameters.
548556
init(
549557
target: ResolvedTarget,
550558
buildParameters: BuildParameters,
551559
pluginInvocationResults: [PluginInvocationResult] = [],
560+
prebuildCommandResults: [PrebuildCommandResult] = [],
552561
isTestTarget: Bool? = nil,
553562
testDiscoveryTarget: Bool = false,
554563
fs: FileSystem = localFileSystem
@@ -562,22 +571,28 @@ public final class SwiftTargetBuildDescription {
562571
self.fs = fs
563572
self.tempsPath = buildParameters.buildPath.appending(component: target.c99name + ".build")
564573
self.derivedSources = Sources(paths: [], root: tempsPath.appending(component: "DerivedSources"))
574+
self.pluginDerivedSources = Sources(paths: [], root: buildParameters.dataPath)
565575
self.pluginInvocationResults = pluginInvocationResults
566-
567-
// Add any derived source paths declared by build-tool plugins that were applied to this target. We do
568-
// this here and not just in the LLBuildManifestBuilder because we need to include them in any situation
569-
// where sources are processed, e.g. when determining names of object files, etc.
570-
for command in pluginInvocationResults.reduce([], { $0 + $1.commands }) {
571-
// Prebuild and postbuild commands are handled outside the build system.
572-
if case .buildToolCommand(_, _, _, _, _, _, _, let derivedSourcePaths) = command {
573-
// TODO: What should we do if we find non-Swift sources here?
574-
for absPath in derivedSourcePaths {
575-
let relPath = absPath.relative(to: self.derivedSources.root)
576-
self.derivedSources.relativePaths.append(relPath)
577-
}
576+
self.prebuildCommandResults = prebuildCommandResults
577+
578+
// Add any derived source files that were declared in any plugin invocations.
579+
for pluginResult in pluginInvocationResults {
580+
// TODO: What should we do if we find non-Swift sources here?
581+
for absPath in pluginResult.derivedSourceFiles {
582+
let relPath = absPath.relative(to: self.pluginDerivedSources.root)
583+
self.pluginDerivedSources.relativePaths.append(relPath)
578584
}
579585
}
580586

587+
// Add any derived source files that were discovered from output directories of prebuild commands.
588+
for result in self.prebuildCommandResults {
589+
// TODO: What should we do if we find non-Swift sources here?
590+
for path in result.derivedSourceFiles {
591+
let relPath = path.relative(to: self.pluginDerivedSources.root)
592+
self.pluginDerivedSources.relativePaths.append(relPath)
593+
}
594+
}
595+
581596
if shouldEmitObjCCompatibilityHeader {
582597
self.moduleMap = try self.generateModuleMap()
583598
}
@@ -861,7 +876,7 @@ public final class SwiftTargetBuildDescription {
861876
stream <<< " },\n"
862877

863878
// Write out the entries for each source file.
864-
let sources = target.sources.paths + derivedSources.paths
879+
let sources = target.sources.paths + derivedSources.paths + pluginDerivedSources.paths
865880
for (idx, source) in sources.enumerated() {
866881
let object = objects[idx]
867882
let objectDir = object.parentDirectory
@@ -1278,6 +1293,10 @@ public class BuildPlan {
12781293
/// The results of invoking any plugins used by targets in this build.
12791294
public let pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]]
12801295

1296+
/// The results of running any prebuild commands for the targets in this build. This includes any derived
1297+
/// source files as well as directories to which any changes should cause us to reevaluate the build plan.
1298+
public let prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]]
1299+
12811300
/// The filesystem to operate on.
12821301
let fileSystem: FileSystem
12831302

@@ -1358,12 +1377,14 @@ public class BuildPlan {
13581377
buildParameters: BuildParameters,
13591378
graph: PackageGraph,
13601379
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
1380+
prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] = [:],
13611381
diagnostics: DiagnosticsEngine,
13621382
fileSystem: FileSystem = localFileSystem
13631383
) throws {
13641384
self.buildParameters = buildParameters
13651385
self.graph = graph
13661386
self.pluginInvocationResults = pluginInvocationResults
1387+
self.prebuildCommandResults = prebuildCommandResults
13671388
self.diagnostics = diagnostics
13681389
self.fileSystem = fileSystem
13691390

@@ -1389,6 +1410,7 @@ public class BuildPlan {
13891410
target: target,
13901411
buildParameters: buildParameters,
13911412
pluginInvocationResults: pluginInvocationResults[target] ?? [],
1413+
prebuildCommandResults: prebuildCommandResults[target] ?? [],
13921414
fs: fileSystem))
13931415
case is ClangTarget:
13941416
targetMap[target] = try .clang(ClangTargetBuildDescription(

Sources/Build/ManifestBuilder.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ 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+
var derivedSourceDirPaths: [AbsolutePath] = []
113+
for result in plan.prebuildCommandResults.values.flatMap({ $0 }) {
114+
derivedSourceDirPaths.append(contentsOf: result.outputDirectories)
115+
}
116+
inputs.append(contentsOf: derivedSourceDirPaths.sorted().map{ Node.directoryStructure($0) })
117+
111118
// FIXME: Need to handle version-specific manifests.
112119
inputs.append(file: package.manifest.path)
113120

@@ -577,7 +584,7 @@ extension LLBuildManifestBuilder {
577584

578585
// Add any build tool commands created by plugins for the target (prebuild and postbuild commands are handled outside the build).
579586
for command in target.pluginInvocationResults.reduce([], { $0 + $1.commands }) {
580-
if case .buildToolCommand(let displayName, let executable, let arguments, _, _, let inputPaths, let outputPaths, _) = command {
587+
if case .buildToolCommand(let displayName, let executable, let arguments, _, _, let inputPaths, let outputPaths) = command {
581588
// Create a shell command to invoke the executable. We include the path of the executable as a dependency.
582589
// FIXME: We will need to extend the addShellCmd() function to also take working directory and environment.
583590
let execPath = AbsolutePath(executable, relativeTo: buildParameters.buildPath)

0 commit comments

Comments
 (0)