Skip to content

Commit ceb3b87

Browse files
authored
[6.1] Fix prebuild commands (#8440) (#8490)
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 95b91f3 commit ceb3b87

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
@@ -36,10 +36,19 @@ public enum PluginAction {
3636
public struct PluginTool {
3737
public let path: AbsolutePath
3838
public let triples: [String]?
39+
public let source: Source
3940

40-
public init(path: AbsolutePath, triples: [String]? = nil) {
41+
public enum Source {
42+
// Built from an executable target
43+
case built
44+
// Brought in from a binary target
45+
case vended
46+
}
47+
48+
public init(path: AbsolutePath, triples: [String]? = nil, source: Source) {
4149
self.path = path
4250
self.triples = triples
51+
self.source = source
4352
}
4453
}
4554

@@ -453,12 +462,14 @@ extension PluginModule {
453462
// Determine additional input dependencies for any plugin commands,
454463
// based on any executables the plugin target depends on.
455464
let toolPaths = accessibleTools.values.map(\.path).sorted()
465+
466+
let builtToolPaths = accessibleTools.values.filter({ $0.source == .built }).map((\.path)).sorted()
456467

457468
let delegate = DefaultPluginInvocationDelegate(
458469
fileSystem: fileSystem,
459470
delegateQueue: delegateQueue,
460471
toolPaths: toolPaths,
461-
builtToolNames: accessibleTools.map(\.key)
472+
builtToolPaths: builtToolPaths
462473
)
463474

464475
let startTime = DispatchTime.now()
@@ -654,15 +665,15 @@ public extension ResolvedModule {
654665
switch tool {
655666
case .builtTool(let name, let path):
656667
if let path = try await builtToolHandler(name, path) {
657-
tools[name] = PluginTool(path: path)
668+
tools[name] = PluginTool(path: path, source: .built)
658669
}
659670
case .vendedTool(let name, let path, let triples):
660671
// Avoid having the path of an unsupported tool overwrite a supported one.
661672
guard !triples.isEmpty || tools[name] == nil else {
662673
continue
663674
}
664675
let priorTriples = tools[name]?.triples ?? []
665-
tools[name] = PluginTool(path: path, triples: priorTriples + triples)
676+
tools[name] = PluginTool(path: path, triples: priorTriples + triples, source: .vended)
666677
}
667678
}
668679

@@ -786,7 +797,7 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
786797
let fileSystem: FileSystem
787798
let delegateQueue: DispatchQueue
788799
let toolPaths: [AbsolutePath]
789-
let builtToolNames: [String]
800+
let builtToolPaths: [AbsolutePath]
790801
var outputData = Data()
791802
var diagnostics = [Basics.Diagnostic]()
792803
var buildCommands = [BuildToolPluginInvocationResult.BuildCommand]()
@@ -796,12 +807,12 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
796807
fileSystem: FileSystem,
797808
delegateQueue: DispatchQueue,
798809
toolPaths: [AbsolutePath],
799-
builtToolNames: [String]
810+
builtToolPaths: [AbsolutePath]
800811
) {
801812
self.fileSystem = fileSystem
802813
self.delegateQueue = delegateQueue
803814
self.toolPaths = toolPaths
804-
self.builtToolNames = builtToolNames
815+
self.builtToolPaths = builtToolPaths
805816
}
806817

807818
func pluginCompilationStarted(commandLine: [String], environment: [String: String]) {}
@@ -855,7 +866,7 @@ final class DefaultPluginInvocationDelegate: PluginInvocationDelegate {
855866
) -> Bool {
856867
dispatchPrecondition(condition: .onQueue(self.delegateQueue))
857868
// executable must exist before running prebuild command
858-
if self.builtToolNames.contains(executable.basename) {
869+
if builtToolPaths.contains(executable) {
859870
self.diagnostics
860871
.append(
861872
.error(

Tests/SPMBuildCoreTests/PluginInvocationTests.swift

+173-2
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,177 @@ final class PluginInvocationTests: XCTestCase {
808808
}
809809
}
810810

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

@@ -1337,7 +1508,7 @@ final class PluginInvocationTests: XCTestCase {
13371508
$0.value.forEach {
13381509
XCTAssertTrue($0.succeeded, "plugin unexpectedly failed")
13391510
XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics")
1340-
XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool\(hostTriple.tripleString).sh")
1511+
XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool")
13411512
}
13421513
}
13431514
}

0 commit comments

Comments
 (0)