Skip to content

Commit b8b545d

Browse files
authored
Fix prebuild commands (#8440)
Somewhere along the way earling in the 6.1 branch changes were made to how the prebuild commands did their check to make sure the executable invoked in the command existed in the scratch dir. It had always used a trick that kept track of whether a tool was a vendored binaryTarget or built. And only succeed if it was vendored. That information was lost in the restructuring. This change stores whether a tool comes from a executable target (built tool) or a binary target (vended) and properly checks to make sure the tool is vended. This was detected when the SwiftLint prebuild command target was failing when a user upgraded to 6.1. This change will need to be cherry picked back to the 6.1 branch for the next point release.
1 parent c4f0e4b commit b8b545d

File tree

2 files changed

+192
-10
lines changed

2 files changed

+192
-10
lines changed

Sources/SPMBuildCore/Plugins/PluginInvocation.swift

+19-8
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,19 @@ public enum PluginAction {
4343
public struct PluginTool {
4444
public let path: AbsolutePath
4545
public let triples: [String]?
46+
public let source: Source
4647

47-
public init(path: AbsolutePath, triples: [String]? = nil) {
48+
public enum Source {
49+
// Built from an executable target
50+
case built
51+
// Brought in from a binary target
52+
case vended
53+
}
54+
55+
public init(path: AbsolutePath, triples: [String]? = nil, source: Source) {
4856
self.path = path
4957
self.triples = triples
58+
self.source = source
5059
}
5160
}
5261

@@ -507,12 +516,14 @@ extension PluginModule {
507516
// Determine additional input dependencies for any plugin commands,
508517
// based on any executables the plugin target depends on.
509518
let toolPaths = accessibleTools.values.map(\.path).sorted()
519+
520+
let builtToolPaths = accessibleTools.values.filter({ $0.source == .built }).map((\.path)).sorted()
510521

511522
let delegate = DefaultPluginInvocationDelegate(
512523
fileSystem: fileSystem,
513524
delegateQueue: delegateQueue,
514525
toolPaths: toolPaths,
515-
builtToolNames: accessibleTools.map(\.key)
526+
builtToolPaths: builtToolPaths
516527
)
517528

518529
let startTime = DispatchTime.now()
@@ -708,15 +719,15 @@ public extension ResolvedModule {
708719
switch tool {
709720
case .builtTool(let name, let path):
710721
if let path = try await builtToolHandler(name, path) {
711-
tools[name] = PluginTool(path: path)
722+
tools[name] = PluginTool(path: path, source: .built)
712723
}
713724
case .vendedTool(let name, let path, let triples):
714725
// Avoid having the path of an unsupported tool overwrite a supported one.
715726
guard !triples.isEmpty || tools[name] == nil else {
716727
continue
717728
}
718729
let priorTriples = tools[name]?.triples ?? []
719-
tools[name] = PluginTool(path: path, triples: priorTriples + triples)
730+
tools[name] = PluginTool(path: path, triples: priorTriples + triples, source: .vended)
720731
}
721732
}
722733

@@ -840,7 +851,7 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
840851
let fileSystem: FileSystem
841852
let delegateQueue: DispatchQueue
842853
let toolPaths: [AbsolutePath]
843-
let builtToolNames: [String]
854+
let builtToolPaths: [AbsolutePath]
844855
var outputData = Data()
845856
var diagnostics = [Basics.Diagnostic]()
846857
var buildCommands = [BuildToolPluginInvocationResult.BuildCommand]()
@@ -850,12 +861,12 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
850861
fileSystem: FileSystem,
851862
delegateQueue: DispatchQueue,
852863
toolPaths: [AbsolutePath],
853-
builtToolNames: [String]
864+
builtToolPaths: [AbsolutePath]
854865
) {
855866
self.fileSystem = fileSystem
856867
self.delegateQueue = delegateQueue
857868
self.toolPaths = toolPaths
858-
self.builtToolNames = builtToolNames
869+
self.builtToolPaths = builtToolPaths
859870
}
860871

861872
func pluginCompilationStarted(commandLine: [String], environment: [String: String]) {}
@@ -909,7 +920,7 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
909920
) -> Bool {
910921
dispatchPrecondition(condition: .onQueue(self.delegateQueue))
911922
// executable must exist before running prebuild command
912-
if self.builtToolNames.contains(executable.basename) {
923+
if builtToolPaths.contains(executable) {
913924
self.diagnostics
914925
.append(
915926
.error(

Tests/BuildTests/PluginInvocationTests.swift

+173-2
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,177 @@ final class PluginInvocationTests: XCTestCase {
810810
}
811811
}
812812

813+
func testPrebuildPluginShouldUseBinaryTarget() async throws {
814+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
815+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
816+
817+
try await testWithTemporaryDirectory { tmpPath in
818+
// Create a sample package with a library target and a plugin.
819+
let packageDir = tmpPath.appending(components: "mypkg")
820+
try localFileSystem.createDirectory(packageDir, recursive: true)
821+
try localFileSystem.writeFileContents(packageDir.appending("Package.swift"), string: """
822+
// swift-tools-version:5.7
823+
824+
import PackageDescription
825+
826+
let package = Package(
827+
name: "mypkg",
828+
products: [
829+
.library(
830+
name: "MyLib",
831+
targets: ["MyLib"])
832+
],
833+
targets: [
834+
.target(
835+
name: "MyLib",
836+
plugins: [
837+
.plugin(name: "X")
838+
]),
839+
.plugin(
840+
name: "X",
841+
capability: .buildTool(),
842+
dependencies: [ "Y" ]
843+
),
844+
.binaryTarget(
845+
name: "Y",
846+
path: "Binaries/Y.\(artifactBundleExtension)"
847+
),
848+
]
849+
)
850+
""")
851+
852+
let libTargetDir = packageDir.appending(components: "Sources", "MyLib")
853+
try localFileSystem.createDirectory(libTargetDir, recursive: true)
854+
try localFileSystem.writeFileContents(libTargetDir.appending("file.swift"), string: """
855+
public struct MyUtilLib {
856+
public let strings: [String]
857+
public init(args: [String]) {
858+
self.strings = args
859+
}
860+
}
861+
""")
862+
863+
let depTargetDir = packageDir.appending(components: "Sources", "Y")
864+
try localFileSystem.createDirectory(depTargetDir, recursive: true)
865+
try localFileSystem.writeFileContents(depTargetDir.appending("main.swift"), string: """
866+
struct Y {
867+
func run() {
868+
print("You passed us two arguments, argumentOne, and argumentTwo")
869+
}
870+
}
871+
Y.main()
872+
""")
873+
874+
let pluginTargetDir = packageDir.appending(components: "Plugins", "X")
875+
try localFileSystem.createDirectory(pluginTargetDir, recursive: true)
876+
try localFileSystem.writeFileContents(pluginTargetDir.appending("plugin.swift"), string: """
877+
import PackagePlugin
878+
@main struct X: BuildToolPlugin {
879+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
880+
[
881+
Command.prebuildCommand(
882+
displayName: "X: Running Y before the build...",
883+
executable: try context.tool(named: "Y").path,
884+
arguments: [ "ARGUMENT_ONE", "ARGUMENT_TWO" ],
885+
outputFilesDirectory: context.pluginWorkDirectory.appending("OUTPUT_FILES_DIRECTORY")
886+
)
887+
]
888+
}
889+
}
890+
""")
891+
892+
let artifactVariants = [try UserToolchain.default.targetTriple].map {
893+
"""
894+
{ "path": "Y", "supportedTriples": ["\($0.tripleString)"] }
895+
"""
896+
}
897+
898+
let bundlePath = packageDir.appending(components: "Binaries", "Y.\(artifactBundleExtension)")
899+
let bundleMetadataPath = bundlePath.appending(component: "info.json")
900+
try localFileSystem.createDirectory(bundleMetadataPath.parentDirectory, recursive: true)
901+
try localFileSystem.writeFileContents(
902+
bundleMetadataPath,
903+
string: """
904+
{ "schemaVersion": "1.0",
905+
"artifacts": {
906+
"Y": {
907+
"type": "executable",
908+
"version": "1.2.3",
909+
"variants": [
910+
\(artifactVariants.joined(separator: ","))
911+
]
912+
}
913+
}
914+
}
915+
"""
916+
)
917+
let binaryPath = bundlePath.appending(component: "Y")
918+
try localFileSystem.writeFileContents(binaryPath, string: "")
919+
920+
// Load a workspace from the package.
921+
let observability = ObservabilitySystem.makeForTesting()
922+
let workspace = try Workspace(
923+
fileSystem: localFileSystem,
924+
forRootPackage: packageDir,
925+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
926+
delegate: MockWorkspaceDelegate()
927+
)
928+
929+
// Load the root manifest.
930+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
931+
let rootManifests = try await workspace.loadRootManifests(
932+
packages: rootInput.packages,
933+
observabilityScope: observability.topScope
934+
)
935+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
936+
937+
// Load the package graph.
938+
let packageGraph = try await workspace.loadPackageGraph(
939+
rootInput: rootInput,
940+
observabilityScope: observability.topScope
941+
)
942+
XCTAssertNoDiagnostics(observability.diagnostics)
943+
XCTAssert(packageGraph.packages.count == 1, "\(packageGraph.packages)")
944+
945+
// Find the build tool plugin.
946+
let buildToolPlugin = try XCTUnwrap(packageGraph.packages.first?.modules.map(\.underlying).filter{ $0.name == "X" }.first as? PluginModule)
947+
XCTAssertEqual(buildToolPlugin.name, "X")
948+
XCTAssertEqual(buildToolPlugin.capability, .buildTool)
949+
950+
// Create a plugin script runner for the duration of the test.
951+
let pluginCacheDir = tmpPath.appending("plugin-cache")
952+
let pluginScriptRunner = DefaultPluginScriptRunner(
953+
fileSystem: localFileSystem,
954+
cacheDir: pluginCacheDir,
955+
toolchain: try UserToolchain.default
956+
)
957+
958+
// Invoke build tool plugin
959+
do {
960+
let outputDir = packageDir.appending(".build")
961+
let buildParameters = mockBuildParameters(
962+
destination: .host,
963+
environment: BuildEnvironment(platform: .macOS, configuration: .debug)
964+
)
965+
966+
let result = try await invokeBuildToolPlugins(
967+
graph: packageGraph,
968+
buildParameters: buildParameters,
969+
fileSystem: localFileSystem,
970+
outputDir: outputDir,
971+
pluginScriptRunner: pluginScriptRunner,
972+
observabilityScope: observability.topScope
973+
)
974+
975+
let diags = result.flatMap(\.value.results).flatMap(\.diagnostics)
976+
testDiagnostics(diags) { result in
977+
result.checkIsEmpty()
978+
}
979+
}
980+
}
981+
}
982+
983+
813984
func testPrebuildPluginShouldNotUseExecTarget() async throws {
814985
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
815986
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
@@ -1203,7 +1374,7 @@ final class PluginInvocationTests: XCTestCase {
12031374
try localFileSystem.writeFileContents(myPluginTargetDir.appending("plugin.swift"), string: content)
12041375
let artifactVariants = artifactSupportedTriples.map {
12051376
"""
1206-
{ "path": "LocalBinaryTool\($0.tripleString).sh", "supportedTriples": ["\($0.tripleString)"] }
1377+
{ "path": "\($0.tripleString)/LocalBinaryTool", "supportedTriples": ["\($0.tripleString)"] }
12071378
"""
12081379
}
12091380

@@ -1339,7 +1510,7 @@ final class PluginInvocationTests: XCTestCase {
13391510
$0.value.forEach {
13401511
XCTAssertTrue($0.succeeded, "plugin unexpectedly failed")
13411512
XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics")
1342-
XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool\(hostTriple.tripleString).sh")
1513+
XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool")
13431514
}
13441515
}
13451516
}

0 commit comments

Comments
 (0)