Skip to content

Commit 20fae14

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

File tree

2 files changed

+162
-2
lines changed

2 files changed

+162
-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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,4 +579,162 @@ class PluginInvocationTests: XCTestCase {
579579
}
580580
}
581581
}
582+
583+
func testPrebuildPluginShouldNotUseExecTarget() throws {
584+
try testWithTemporaryDirectory { tmpPath in
585+
// Create a sample package with a library target and a plugin.
586+
let packageDir = tmpPath.appending(components: "mypkg")
587+
try localFileSystem.createDirectory(packageDir, recursive: true)
588+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
589+
// swift-tools-version:5.6
590+
591+
import PackageDescription
592+
593+
let package = Package(
594+
name: "mypkg",
595+
products: [
596+
.library(
597+
name: "MyLib",
598+
targets: ["MyLib"])
599+
],
600+
targets: [
601+
.target(
602+
name: "MyLib",
603+
plugins: [
604+
.plugin(name: "X")
605+
]),
606+
.plugin(
607+
name: "X",
608+
capability: .buildTool(),
609+
dependencies: [ "Y" ]
610+
),
611+
.executableTarget(
612+
name: "Y",
613+
dependencies: []),
614+
]
615+
)
616+
""")
617+
618+
let libTargetDir = packageDir.appending(components: "Sources", "MyLib")
619+
try localFileSystem.createDirectory(libTargetDir, recursive: true)
620+
try localFileSystem.writeFileContents(libTargetDir.appending(component: "file.swift"), string: """
621+
public struct MyUtilLib {
622+
public let strings: [String]
623+
public init(args: [String]) {
624+
self.strings = args
625+
}
626+
}
627+
""")
628+
629+
let depTargetDir = packageDir.appending(components: "Sources", "Y")
630+
try localFileSystem.createDirectory(depTargetDir, recursive: true)
631+
try localFileSystem.writeFileContents(depTargetDir.appending(component: "main.swift"), string: """
632+
struct Y {
633+
func run() {
634+
print("You passed us two arguments, argumentOne, and argumentTwo")
635+
}
636+
}
637+
Y.main()
638+
""")
639+
640+
let pluginTargetDir = packageDir.appending(components: "Plugins", "X")
641+
try localFileSystem.createDirectory(pluginTargetDir, recursive: true)
642+
try localFileSystem.writeFileContents(pluginTargetDir.appending(component: "plugin.swift"), string: """
643+
import PackagePlugin
644+
@main struct X: BuildToolPlugin {
645+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
646+
[
647+
Command.prebuildCommand(
648+
displayName: "X: Running SomeCommand before the build...",
649+
executable: try context.tool(named: "Y").path,
650+
arguments: [ "ARGUMENT_ONE", "ARGUMENT_TWO" ],
651+
outputFilesDirectory: context.pluginWorkDirectory.appending("OUTPUT_FILES_DIRECTORY")
652+
)
653+
]
654+
}
655+
656+
}
657+
""")
658+
659+
// Load a workspace from the package.
660+
let observability = ObservabilitySystem.makeForTesting()
661+
let workspace = try Workspace(
662+
fileSystem: localFileSystem,
663+
forRootPackage: packageDir,
664+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
665+
delegate: MockWorkspaceDelegate()
666+
)
667+
668+
// Load the root manifest.
669+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
670+
let rootManifests = try tsc_await {
671+
workspace.loadRootManifests(
672+
packages: rootInput.packages,
673+
observabilityScope: observability.topScope,
674+
completion: $0
675+
)
676+
}
677+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
678+
679+
// Load the package graph.
680+
let packageGraph = try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)
681+
XCTAssertNoDiagnostics(observability.diagnostics)
682+
XCTAssert(packageGraph.packages.count == 1, "\(packageGraph.packages)")
683+
684+
// Find the build tool plugin.
685+
let buildToolPlugin = try XCTUnwrap(packageGraph.packages[0].targets.map(\.underlyingTarget).first{ $0.name == "X" } as? PluginTarget)
686+
XCTAssertEqual(buildToolPlugin.name, "X")
687+
XCTAssertEqual(buildToolPlugin.capability, .buildTool)
688+
689+
// Create a plugin script runner for the duration of the test.
690+
let pluginCacheDir = tmpPath.appending(component: "plugin-cache")
691+
let pluginScriptRunner = DefaultPluginScriptRunner(
692+
fileSystem: localFileSystem,
693+
cacheDir: pluginCacheDir,
694+
toolchain: try UserToolchain.default
695+
)
696+
697+
// Define a plugin compilation delegate that just captures the passed information.
698+
class Delegate: PluginScriptCompilerDelegate {
699+
var commandLine: [String]?
700+
var environment: EnvironmentVariables?
701+
var compiledResult: PluginCompilationResult?
702+
var cachedResult: PluginCompilationResult?
703+
init() {
704+
}
705+
func willCompilePlugin(commandLine: [String], environment: EnvironmentVariables) {
706+
self.commandLine = commandLine
707+
self.environment = environment
708+
}
709+
func didCompilePlugin(result: PluginCompilationResult) {
710+
self.compiledResult = result
711+
}
712+
func skippedCompilingPlugin(cachedResult: PluginCompilationResult) {
713+
self.cachedResult = cachedResult
714+
}
715+
}
716+
717+
// Try to compile the plugin script.
718+
do {
719+
// Invoke build tool plugin
720+
let outputDir = packageDir.appending(component: ".build")
721+
let builtToolsDir = outputDir.appending(component: "debug")
722+
let _ = try packageGraph.invokeBuildToolPlugins(
723+
outputDir: outputDir,
724+
builtToolsDir: builtToolsDir,
725+
buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug),
726+
toolSearchDirectories: [UserToolchain.default.swiftCompilerPath.parentDirectory],
727+
pluginScriptRunner: pluginScriptRunner,
728+
observabilityScope: observability.topScope,
729+
fileSystem: localFileSystem
730+
)
731+
732+
testDiagnostics(observability.diagnostics) { result in
733+
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"
734+
result.check(diagnostic: .contains(msg), severity: .error)
735+
}
736+
}
737+
738+
}
739+
}
582740
}

0 commit comments

Comments
 (0)