Skip to content

Commit 519e88e

Browse files
committed
Add diags for un-prebuilt exec target used by prebuild plugin
Resolves rdar://90442392
1 parent d98de63 commit 519e88e

File tree

2 files changed

+337
-2
lines changed

2 files changed

+337
-2
lines changed

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ extension PluginTarget {
188188
outputFiles: try outputFiles.map{ try AbsolutePath(validating: $0) })
189189

190190
case .definePrebuildCommand(let config, let outputFilesDir):
191+
let execPath = try AbsolutePath(validating: config.executable)
192+
if !FileManager.default.fileExists(atPath: execPath.pathString) {
193+
observabilityScope.emit(error: "exectuable target '\(execPath.basename)' is not pre-built; a plugin running a prebuild command should only rely on a pre-built binary; as a workaround, build '\(execPath.basename)' first and then run the plugin")
194+
}
191195
self.invocationDelegate.pluginDefinedPrebuildCommand(
192196
displayName: config.displayName,
193197
executable: try AbsolutePath(validating: config.executable),
@@ -288,7 +292,6 @@ fileprivate extension PluginToHostMessage {
288292
}
289293
}
290294

291-
292295
extension PackageGraph {
293296

294297
/// Traverses the graph of reachable targets in a package graph, and applies plugins to targets as needed. Each
@@ -613,7 +616,6 @@ public enum PluginEvaluationError: Swift.Error {
613616
case decodingPluginOutputFailed(json: Data, underlyingError: Error)
614617
}
615618

616-
617619
public protocol PluginInvocationDelegate {
618620
/// Called before a plugin is compiled. This call is always followed by a `pluginCompilationEnded()`, but is mutually exclusive with `pluginCompilationWasSkipped()` (which is called if the plugin didn't need to be recompiled).
619621
func pluginCompilationStarted(commandLine: [String], environment: EnvironmentVariables)

Tests/SPMBuildCoreTests/PluginInvocationTests.swift

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,4 +579,337 @@ class PluginInvocationTests: XCTestCase {
579579
}
580580
}
581581
}
582+
583+
func testUnsupportedDependencyProduct() throws {
584+
try testWithTemporaryDirectory { tmpPath in
585+
// Create a sample package with a library target and a plugin.
586+
let packageDir = tmpPath.appending(components: "MyPackage")
587+
try localFileSystem.createDirectory(packageDir, recursive: true)
588+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
589+
// swift-tools-version: 5.7
590+
import PackageDescription
591+
let package = Package(
592+
name: "MyPackage",
593+
dependencies: [
594+
.package(path: "../FooPackage"),
595+
],
596+
targets: [
597+
.plugin(
598+
name: "MyPlugin",
599+
capability: .buildTool(),
600+
dependencies: [
601+
.product(name: "FooLib", package: "FooPackage"),
602+
]
603+
),
604+
]
605+
)
606+
""")
607+
608+
let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin")
609+
try localFileSystem.createDirectory(myPluginTargetDir, recursive: true)
610+
try localFileSystem.writeFileContents(myPluginTargetDir.appending(component: "plugin.swift"), string: """
611+
import PackagePlugin
612+
import Foo
613+
@main struct MyBuildToolPlugin: BuildToolPlugin {
614+
func createBuildCommands(
615+
context: PluginContext,
616+
target: Target
617+
) throws -> [Command] { }
618+
}
619+
""")
620+
621+
let fooPkgDir = tmpPath.appending(components: "FooPackage")
622+
try localFileSystem.createDirectory(fooPkgDir, recursive: true)
623+
try localFileSystem.writeFileContents(fooPkgDir.appending(component: "Package.swift"), string: """
624+
// swift-tools-version: 5.7
625+
import PackageDescription
626+
let package = Package(
627+
name: "FooPackage",
628+
products: [
629+
.library(name: "FooLib",
630+
targets: ["Foo"]),
631+
],
632+
targets: [
633+
.target(
634+
name: "Foo",
635+
dependencies: []
636+
),
637+
]
638+
)
639+
""")
640+
let fooTargetDir = fooPkgDir.appending(components: "Sources", "Foo")
641+
try localFileSystem.createDirectory(fooTargetDir, recursive: true)
642+
try localFileSystem.writeFileContents(fooTargetDir.appending(component: "file.swift"), string: """
643+
public func foo() { }
644+
""")
645+
646+
// Load a workspace from the package.
647+
let observability = ObservabilitySystem.makeForTesting()
648+
let workspace = try Workspace(
649+
fileSystem: localFileSystem,
650+
forRootPackage: packageDir,
651+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
652+
delegate: MockWorkspaceDelegate()
653+
)
654+
655+
// Load the root manifest.
656+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
657+
let rootManifests = try tsc_await {
658+
workspace.loadRootManifests(
659+
packages: rootInput.packages,
660+
observabilityScope: observability.topScope,
661+
completion: $0
662+
)
663+
}
664+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
665+
666+
// Load the package graph.
667+
XCTAssertThrowsError(try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)) { error in
668+
var diagnosed = false
669+
if let realError = error as? PackageGraphError,
670+
realError.description == "target 'MyPlugin' of type 'plugin' cannot depend on 'FooLib' of type 'library' from package 'foopackage'; this dependency is unsupported" {
671+
diagnosed = true
672+
}
673+
XCTAssertTrue(diagnosed)
674+
}
675+
}
676+
}
677+
678+
func testUnsupportedDependencyTarget() throws {
679+
try testWithTemporaryDirectory { tmpPath in
680+
// Create a sample package with a library target and a plugin.
681+
let packageDir = tmpPath.appending(components: "MyPackage")
682+
try localFileSystem.createDirectory(packageDir, recursive: true)
683+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
684+
// swift-tools-version: 5.7
685+
import PackageDescription
686+
let package = Package(
687+
name: "MyPackage",
688+
dependencies: [
689+
.package(path: "../FooPackage"),
690+
],
691+
targets: [
692+
.target(
693+
name: "MyLibrary",
694+
dependencies: []
695+
),
696+
.plugin(
697+
name: "MyPlugin",
698+
capability: .buildTool(),
699+
dependencies: [
700+
"MyLibrary",
701+
.product(name: "FooLib", package: "FooPackage"),
702+
]
703+
),
704+
]
705+
)
706+
""")
707+
708+
let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary")
709+
try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true)
710+
try localFileSystem.writeFileContents(myLibraryTargetDir.appending(component: "library.swift"), string: """
711+
public func hello() { }
712+
""")
713+
let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin")
714+
try localFileSystem.createDirectory(myPluginTargetDir, recursive: true)
715+
try localFileSystem.writeFileContents(myPluginTargetDir.appending(component: "plugin.swift"), string: """
716+
import PackagePlugin
717+
import MyLibrary
718+
@main struct MyBuildToolPlugin: BuildToolPlugin {
719+
func createBuildCommands(
720+
context: PluginContext,
721+
target: Target
722+
) throws -> [Command] { }
723+
}
724+
""")
725+
726+
// Load a workspace from the package.
727+
let observability = ObservabilitySystem.makeForTesting()
728+
let workspace = try Workspace(
729+
fileSystem: localFileSystem,
730+
forRootPackage: packageDir,
731+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
732+
delegate: MockWorkspaceDelegate()
733+
)
734+
735+
// Load the root manifest.
736+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
737+
let rootManifests = try tsc_await {
738+
workspace.loadRootManifests(
739+
packages: rootInput.packages,
740+
observabilityScope: observability.topScope,
741+
completion: $0
742+
)
743+
}
744+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
745+
746+
// Load the package graph.
747+
XCTAssertThrowsError(try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)) { error in
748+
var diagnosed = false
749+
if let realError = error as? PackageGraphError,
750+
realError.description == "target 'MyPlugin' of type 'plugin' cannot depend on 'MyLibrary' of type 'library'; this dependency is unsupported" {
751+
diagnosed = true
752+
}
753+
XCTAssertTrue(diagnosed)
754+
}
755+
}
756+
}
757+
758+
func testPrebuildPluginShouldNotUseExecTarget() throws {
759+
try testWithTemporaryDirectory { tmpPath in
760+
// Create a sample package with a library target and a plugin.
761+
let packageDir = tmpPath.appending(components: "mypkg")
762+
try localFileSystem.createDirectory(packageDir, recursive: true)
763+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
764+
// swift-tools-version:5.6
765+
766+
import PackageDescription
767+
768+
let package = Package(
769+
name: "mypkg",
770+
products: [
771+
.library(
772+
name: "MyLib",
773+
targets: ["MyLib"])
774+
],
775+
targets: [
776+
.target(
777+
name: "MyLib",
778+
plugins: [
779+
.plugin(name: "X")
780+
]),
781+
.plugin(
782+
name: "X",
783+
capability: .buildTool(),
784+
dependencies: [ "Y" ]
785+
),
786+
.executableTarget(
787+
name: "Y",
788+
dependencies: []),
789+
]
790+
)
791+
""")
792+
793+
let libTargetDir = packageDir.appending(components: "Sources", "MyLib")
794+
try localFileSystem.createDirectory(libTargetDir, recursive: true)
795+
try localFileSystem.writeFileContents(libTargetDir.appending(component: "file.swift"), string: """
796+
public struct MyUtilLib {
797+
public let strings: [String]
798+
public init(args: [String]) {
799+
self.strings = args
800+
}
801+
}
802+
""")
803+
804+
let depTargetDir = packageDir.appending(components: "Sources", "Y")
805+
try localFileSystem.createDirectory(depTargetDir, recursive: true)
806+
try localFileSystem.writeFileContents(depTargetDir.appending(component: "main.swift"), string: """
807+
struct Y {
808+
func run() {
809+
print("You passed us two arguments, argumentOne, and argumentTwo")
810+
}
811+
}
812+
Y.main()
813+
""")
814+
815+
let pluginTargetDir = packageDir.appending(components: "Plugins", "X")
816+
try localFileSystem.createDirectory(pluginTargetDir, recursive: true)
817+
try localFileSystem.writeFileContents(pluginTargetDir.appending(component: "plugin.swift"), string: """
818+
import PackagePlugin
819+
@main struct X: BuildToolPlugin {
820+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
821+
[
822+
Command.prebuildCommand(
823+
displayName: "X: Running SomeCommand before the build...",
824+
executable: try context.tool(named: "Y").path,
825+
arguments: [ "ARGUMENT_ONE", "ARGUMENT_TWO" ],
826+
outputFilesDirectory: context.pluginWorkDirectory.appending("OUTPUT_FILES_DIRECTORY")
827+
)
828+
]
829+
}
830+
831+
}
832+
""")
833+
834+
// Load a workspace from the package.
835+
let observability = ObservabilitySystem.makeForTesting()
836+
let workspace = try Workspace(
837+
fileSystem: localFileSystem,
838+
forRootPackage: packageDir,
839+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
840+
delegate: MockWorkspaceDelegate()
841+
)
842+
843+
// Load the root manifest.
844+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
845+
let rootManifests = try tsc_await {
846+
workspace.loadRootManifests(
847+
packages: rootInput.packages,
848+
observabilityScope: observability.topScope,
849+
completion: $0
850+
)
851+
}
852+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
853+
854+
// Load the package graph.
855+
let packageGraph = try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)
856+
XCTAssertNoDiagnostics(observability.diagnostics)
857+
XCTAssert(packageGraph.packages.count == 1, "\(packageGraph.packages)")
858+
859+
// Find the build tool plugin.
860+
let buildToolPlugin = try XCTUnwrap(packageGraph.packages[0].targets.map(\.underlyingTarget).first{ $0.name == "X" } as? PluginTarget)
861+
XCTAssertEqual(buildToolPlugin.name, "X")
862+
XCTAssertEqual(buildToolPlugin.capability, .buildTool)
863+
864+
// Create a plugin script runner for the duration of the test.
865+
let pluginCacheDir = tmpPath.appending(component: "plugin-cache")
866+
let pluginScriptRunner = DefaultPluginScriptRunner(
867+
fileSystem: localFileSystem,
868+
cacheDir: pluginCacheDir,
869+
toolchain: try UserToolchain.default
870+
)
871+
872+
// Define a plugin compilation delegate that just captures the passed information.
873+
class Delegate: PluginScriptCompilerDelegate {
874+
var commandLine: [String]?
875+
var environment: EnvironmentVariables?
876+
var compiledResult: PluginCompilationResult?
877+
var cachedResult: PluginCompilationResult?
878+
init() {
879+
}
880+
func willCompilePlugin(commandLine: [String], environment: EnvironmentVariables) {
881+
self.commandLine = commandLine
882+
self.environment = environment
883+
}
884+
func didCompilePlugin(result: PluginCompilationResult) {
885+
self.compiledResult = result
886+
}
887+
func skippedCompilingPlugin(cachedResult: PluginCompilationResult) {
888+
self.cachedResult = cachedResult
889+
}
890+
}
891+
892+
// Try to compile the plugin script.
893+
do {
894+
// Invoke build tool plugin
895+
let outputDir = packageDir.appending(component: ".build")
896+
let builtToolsDir = outputDir.appending(component: "debug")
897+
let _ = try packageGraph.invokeBuildToolPlugins(
898+
outputDir: outputDir,
899+
builtToolsDir: builtToolsDir,
900+
buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug),
901+
toolSearchDirectories: [UserToolchain.default.swiftCompilerPath.parentDirectory],
902+
pluginScriptRunner: pluginScriptRunner,
903+
observabilityScope: observability.topScope,
904+
fileSystem: localFileSystem
905+
)
906+
907+
testDiagnostics(observability.diagnostics) { result in
908+
let msg = "exectuable target 'Y' is not pre-built; a plugin running a prebuild command should only rely on a pre-built binary; as a workaround, build 'Y' first and then run the plugin"
909+
result.check(diagnostic: .contains(msg), severity: .error)
910+
}
911+
}
912+
913+
}
914+
}
582915
}

0 commit comments

Comments
 (0)