From 8d7a04734d9f827bbd6ea96ece9e1a243c92b092 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Fri, 19 Jan 2024 17:11:51 +0000 Subject: [PATCH 1/2] command plugins: Build command plugin dependencies for the host, not the target Since #7164, dependencies of command plugins are once again being built for the _target_ rather than the host. This causes problem when cross compiling because the host needs to be able to run the plugin dependencies, but finds target binaries instead. This problem was fixed before in #6791 by forcing command plugin dependencies to be built for the host by overriding the default build parameters in swiftTool.createBuildSystem(). The same solution still works in this commit, but a better long-term option would be to rework BuildOperation.plan() to handle command plugin dependencies specially, as it already does for build plugin dependencies. At present, BuildOperation.plan calls graph.invokeBuildToolPlugins to process sources. invokeBuildToolPlugins finds all build tool dependecies and builds them separately, using a specially-created BuildOperation instance: https://github.com/apple/swift-package-manager/blob/34efc0bfe9d40d9a019644ac8fcd0b852c491dfe/Sources/SPMBuildCore/Plugins/PluginInvocation.swift#L409 There is no equivalent step for command plugin dependencies, so they are built for the host architecture. Ideally we should rework BuildOperation.plan to build command and build plugin dependencies in the same way. --- Sources/Commands/PackageTools/PluginCommand.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Commands/PackageTools/PluginCommand.swift b/Sources/Commands/PackageTools/PluginCommand.swift index 839b9f17b52..1968dae8183 100644 --- a/Sources/Commands/PackageTools/PluginCommand.swift +++ b/Sources/Commands/PackageTools/PluginCommand.swift @@ -319,7 +319,11 @@ struct PluginCommand: SwiftCommand { // Build or bring up-to-date any executable host-side tools on which this plugin depends. Add them and any binary dependencies to the tool-names-to-path map. let buildSystem = try swiftTool.createBuildSystem( explicitBuildSystem: .native, - cacheBuildManifest: false + cacheBuildManifest: false, + // Force all dependencies to be built for the host, to work around the fact that BuildOperation.plan + // knows to compile build tool plugin dependencies for the host but does not do the same for command + // plugins. + productsBuildParameters: buildParameters ) let accessibleTools = try plugin.processAccessibleTools( packageGraph: packageGraph, From cb58af8d6f0229c3f02298a2d49e5dda9a96c1ee Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Tue, 23 Jan 2024 12:09:42 +0000 Subject: [PATCH 2/2] command plugins: Test dependencies are always built for the host This integration test checks that any targets depended on by a command plugin are built for the host, not for the target. * A new CommandPluginTestStub plugin has a dependency on a target executable which will be built automatically when the plugin is run. The test checks that the dependency is built for the host architecture, no matter which target architecture is selected using '--triple'. * The plugin also asks SwiftPM to build the 'placeholder' main target. The test checks that the dependency is built for the target architecture. The test is restricted to macOS because we can be sure of having a viable cross-compilation environment (arm64 to x86_64 and vice versa). The standard Linux build environments can't cross compile to other architectures. --- .../CommandPluginTestStub/Package.swift | 13 +++++++ .../plugin-dependencies-stub/main.swift | 13 +++++++ .../Sources/{ => placeholder}/main.swift | 0 .../Sources/plugintool/main.swift | 1 + Tests/BuildTests/PluginsBuildPlanTests.swift | 39 +++++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift rename Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/{ => placeholder}/main.swift (100%) create mode 100644 Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift index fe69a01f0b5..dcbf3a5e017 100644 --- a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift @@ -19,8 +19,21 @@ let package = Package( description: "Build a target for testing" )) ), + .plugin( + name: "plugin-dependencies-stub", + capability: .command(intent: .custom( + verb: "build-plugin-dependency", + description: "Build a plugin dependency for testing" + )), + dependencies: [ + .target(name: "plugintool") + ] + ), .executableTarget( name: "placeholder" ), + .executableTarget( + name: "plugintool" + ), ] ) diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift new file mode 100644 index 00000000000..7034dbc528b --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift @@ -0,0 +1,13 @@ +import PackagePlugin + +@main +struct test: CommandPlugin { + // This plugin exists to test that the executable it requires is built correctly when cross-compiling + func performCommand(context: PluginContext, arguments: [String]) async throws { + print("Hello from dependencies-stub") + let _ = try packageManager.build( + .product("placeholder"), + parameters: .init(configuration: .debug, logging: .concise) + ) + } +} \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/placeholder/main.swift similarity index 100% rename from Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/main.swift rename to Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/placeholder/main.swift diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift new file mode 100644 index 00000000000..3d2f1a1f757 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift @@ -0,0 +1 @@ +print("Hello from plugintool") diff --git a/Tests/BuildTests/PluginsBuildPlanTests.swift b/Tests/BuildTests/PluginsBuildPlanTests.swift index a98070e0817..5ef99e3fc59 100644 --- a/Tests/BuildTests/PluginsBuildPlanTests.swift +++ b/Tests/BuildTests/PluginsBuildPlanTests.swift @@ -12,7 +12,9 @@ import Basics import SPMTestSupport +@testable import SPMBuildCore import XCTest +import PackageModel final class PluginsBuildPlanTests: XCTestCase { func testBuildToolsDatabasePath() throws { @@ -22,4 +24,41 @@ final class PluginsBuildPlanTests: XCTestCase { XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/plugins/tools/build.db")))) } } + + func testCommandPluginDependenciesWhenCrossCompiling() throws { + // Command Plugin dependencies must be built for the host. + // This test is only supported on macOS because that is the only + // platform on which we can currently be sure of having a viable + // cross-compilation environment (arm64->x86_64 or vice versa). + // On Linux it is typically only possible to build for the host + // environment unless cross-compilation SDKs are being used. + #if !os(macOS) + try XCTSkipIf(true, "test is only supported on macOS") + #endif + + let hostToolchain = try UserToolchain(swiftSDK: .hostSwiftSDK()) + let hostTriple = try! hostToolchain.targetTriple.withoutVersion().tripleString + + let x86Triple = "x86_64-apple-macosx" + let armTriple = "arm64-apple-macosx" + let targetTriple = hostToolchain.targetTriple.arch == .aarch64 ? x86Triple : armTriple + + // By default, plugin dependencies are built for the host platform + try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try executeSwiftPackage(fixturePath, extraArgs: ["-v", "build-plugin-dependency"]) + XCTAssertMatch(stdout, .contains("Hello from dependencies-stub")) + XCTAssertMatch(stderr, .contains("Build of product 'plugintool' complete!")) + XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/plugintool")))) + XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/placeholder")))) + } + + // When cross compiling the final product, plugin dependencies should still be built for the host + try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in + let (stdout, stderr) = try executeSwiftPackage(fixturePath, extraArgs: ["--triple", targetTriple, "-v", "build-plugin-dependency"]) + XCTAssertMatch(stdout, .contains("Hello from dependencies-stub")) + XCTAssertMatch(stderr, .contains("Build of product 'plugintool' complete!")) + XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/\(hostTriple)/debug/plugintool")))) + XCTAssertTrue(localFileSystem.exists(fixturePath.appending(RelativePath(".build/\(targetTriple)/debug/placeholder")))) + } + } }