Skip to content

plugins: Build command plugin dependencies for the host, not the target #6791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

euanh
Copy link
Contributor

@euanh euanh commented Aug 9, 2023

Motivation:

When cross-compiling, SwiftPM plugins and their dependencies run on the host and so must be compiled for the host OS and architecture, not the cross-compiled target OS and architecture. SwiftPM will
compile a command plugin for the host but if the plugin depends on an executable it will cross-compile the executable for the target so the plugin will not be able to run it:

error: Error Domain=NSPOSIXErrorDomain Code=8 "Exec format error"

This problem does not affect binaryTarget dependencies, which are handled by a different code path which correctly selects the the binary for the host system from the archive.

Modifications:

Command plugin dependencies are already handled specially in PluginCommand.run; this commit makes that special build step use the host toolchain instead of the target toolchain.

#6060 handled the equivalent problem for build tool plugins.

Result:

Executables which are are dependencies of command plugins will be built for the host OS and architecture, so the plugins will be able to run them.

Copy link
Contributor

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we come up with a test for it in SwiftToolTests.swift?

When cross-compiling, SwiftPM plugins and their dependencies run
on the host and so must be compiled for the host OS and architecture,
not the cross-compiled target OS and architecture.   SwiftPM will
compile a command plugin for the host but if the plugin depends on
an executable it will cross-compile the executable for the target,
so the plugin will not be able to run it:

    error: Error Domain=NSPOSIXErrorDomain Code=8 "Exec format error"

Command plugin dependencies are already handled specially in
PluginCommand.run; this commit makes that special build step use
the host toolchain instead of the target toolchain.

swiftlang#6060 handled the
equivalent problem for build tool plugins.
@euanh euanh force-pushed the build-command-plugin-dependencies-for-host branch from f5162c8 to 75b18db Compare August 10, 2023 08:39
@euanh
Copy link
Contributor Author

euanh commented Aug 10, 2023

Can we come up with a test for it in SwiftToolTests.swift?

I think regression testing for the normal case where we are not cross-compiling is already covered. I have been testing locally with a test case which turns out to be very similar to testLocalAndRemoteToolDependencies(): a command plugin which depends on an executable target. I would expect this test to fail if the plugin dependency build flow was broken.

One difference is that my local test also has a top-level target which we can check is cross-compiled, in addition to the command plugin which should be built for the host. However I don't think we yet have testing infrastructure to set up a cross-compiler. Is there a way to fake enough of a cross-compliation SDK to show that swift package does the right thing in this case?

I'd be happy to add my test but I think it would just duplicate what's already there.

@euanh euanh requested a review from MaxDesiatov August 10, 2023 14:35
@MaxDesiatov
Copy link
Contributor

@swift-ci smoke test

@euanh
Copy link
Contributor Author

euanh commented Aug 10, 2023

For reference, here is the test I've been using locally: CommandPluginDependencies.zip

If you run the plugin without a cross-compilation SDK you should see the following output:

% swift package plugin my-command-plugin
Building for debugging...
[1/3] Emitting module MyHelperToolLib
[2/3] Compiling MyHelperToolLib lib.swift
[3/5] Compiling MyHelperTool main.swift
[4/5] Emitting module MyHelperTool
[4/5] Linking MyHelperTool
Build complete! (1.75s)
Hello from MyHelperTool
Plugin succeded!

If you run it with a cross-compilation SDK without the fix in this PR, it will fail because MyHelperTool will be built for the cross-compilation target:

% swift package --experimental-swift-sdk $X86_LINUX_SDK plugin my-command-plugin 
Building for debugging...
[1/5] Compiling MyHelperToolLib lib.swift
[2/5] Emitting module MyHelperToolLib
[4/7] Wrapping AST for MyHelperToolLib for debugging
[6/9] Compiling MyHelperTool main.swift
[7/9] Emitting module MyHelperTool
[9/11] Wrapping AST for MyHelperTool for debugging
[10/11] Linking MyHelperTool
Build complete! (4.13s)
error: Error Domain=NSPOSIXErrorDomain Code=8 "Exec format error"

This is because both MyService and MyHelperTool have been cross-compiled for the target and can't run on the host:

% file .build/x86_64-unknown-linux-gnu/debug/MyService .build/x86_64-unknown-linux-gnu/debug/MyHelperTool
.build/x86_64-unknown-linux-gnu/debug/MyService:    ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
.build/x86_64-unknown-linux-gnu/debug/MyHelperTool: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped

If you apply the fix in this PR, the build will succeed. MyService will still be cross-compiled, but MyHelperTool will be built for the host:

% file .build/x86_64-unknown-linux-gnu/debug/MyService .build/x86_64-apple-macosx/debug/MyHelperTool
.build/x86_64-unknown-linux-gnu/debug/MyService: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
.build/x86_64-apple-macosx/debug/MyHelperTool:   Mach-O 64-bit executable x86_64

Copy link
Contributor

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this! SGTM as it was tested manually. We certainly should improve SwiftTool testing infrastructure to cover cross-compilation in the future.

@euanh euanh enabled auto-merge (squash) August 10, 2023 16:31
@euanh euanh merged commit 1daaa1c into swiftlang:main Aug 10, 2023
euanh added a commit to euanh/swift-package-manager that referenced this pull request Jan 22, 2024
…the target

Since swiftlang#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 swiftlang#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.
euanh added a commit to euanh/swift-package-manager that referenced this pull request Jan 22, 2024
…the target

Since swiftlang#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 swiftlang#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.
euanh added a commit to euanh/swift-package-manager that referenced this pull request Jan 22, 2024
…the target

Since swiftlang#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 swiftlang#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.
euanh added a commit that referenced this pull request Jan 23, 2024
…the target (#7280)

Always build command line plugin dependencies for the host triple.

### Motivation:

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.

### Modifications:

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. This commit forces all plugin dependencies to be built for
the host - this is similar to what was done in #6791 and #7273.
 
### Testing:
  
An 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.

### Result:

Command plugins can be used again when cross-compiling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Swift plugins are incorrectly built using cross compilation sdk
3 participants