How to support cross-compilation in plugins

I implemented support for cross-compiling when using the Go plugin as well as cross-compiling using LXD container. On the part of the user of snapcraft it’s completely transparent but each plugin needs to explicitly support this, so here’s how you enable your favorite snapcraft plugin to make this work.

snapcraft --target-arch=armhf --debug

This will result in the following error if a part uses a plugin that doesn’t support cross-compilation yet:

Setting target machine to ‘armhf’
Building for a different target architecture requires a plugin specific implementation in the ‘foo’ plugin

Let’s enable the fictional foo plugin.

snapcraft/plugins/foo.py

First of all, we tell snapcraft that we know how to cross-compile by implementing one additional method. We don’t need to do anything here. Although I’ve found one good use of the method to be checking that the requested target is supported by the toolchain. For a language like Rust or Go the target is different to the Debian architecture. Rust even supports using a different libc (glibc or musl). That means the plugin needs to map the target and raise an error if it can’t. In the following example I’ll also be seeting self._target so that it can be used in other steps of the lifecycle, for example to set environment variables or when installing target-specific dependencies of the toolchain.

def enable_cross_compilation(self):
    # Cf. rustc --print target-list
    rust_targets = {
        'armhf': 'armv7-{}-{}eabihf',
        'arm64': 'aarch64-{}-{}',
        'i386': 'i686-{}-{}',
        'amd64': 'x86_64-{}-{}',
        'ppc64el': 'powerpc64le-{}-{}',
    }
    self._target = rust_targets.get(self.project.deb_arch).format(
        'unknown-linux', 'gnu')
    if not self._target:
        raise NotImplementedError(
            '{!r} is not supported as a target architecture when '
            'cross-compiling with the rust plugin'.format(
                self.project.deb_arch))

Second of all, we’ll want to adjust the environment variables. This depends on the build system. Typical candidates are compiler names and pkgconfig - we don’t need to worry about the latter since snapcraft already takes care of it. Go for example would also require GOARCH to be set, in the following code I’m assuming there’s a self._goarch variable that was set in enable_cross_compilation.

# Some plugins name this _build_env
# If it doesn't exist yet, add it and use it in def build(self)
def _build_environment(self):
    env = os.environ.copy()
    env['CC'] = '{}-gcc'.format(self.project.arch_triplet)
    env['CXX'] = '{}-g++'.format(self.project.arch_triplet)
    env['GOARCH'] = self._goarch

The example uses the triplet of the target architecture. Commonly useful values here are, using armhf values for demonstration purposes:

cross_compiler_prefix: arm-linux-gnueabihf-
arch_triplet: arm-linux-gnueabihf
deb_arch: armhf
kernel_arch: arm

Note that snapcraft will automatically install the compiler and libc headers needed. Should you need extra packages it’s only one command away:

self.build_packages.append('foo-armhf-cross')

It goes without saying that we’ll want to have an integration test checking that this new code actually continues to work in the future.

integration_tests/plugins/test_foo_plugin.py

You’ll need to have a snap in integration_tests/snaps/foo which you may already have. Otherwise you’ll need to add it. It’s a good idea to try and make use of more advanced use cases that may need special care in the context of cross-compilation, such as using C code in the case of Go.

import snapcraft
from snapcraft.tests.matchers import HasArchitecture
[...]
def test_cross_compiling(self):
    if snapcraft.ProjectOptions().deb_arch != 'amd64':
        self.skipTest('The test only handles amd64 to armhf')

    arch = 'arm64'
    self.run_snapcraft('stage', 'foo', '--target={}'.format(arch))
    binary = os.path.join(self.parts_dir, 'foo', 'install', 'bin',
                          os.path.basename(self.path))
    self.assertThat(binary, HasArchitecture('aarch64'))

You’ll also want to unit test the new use cases supported by the plugin. There will be a snapcraft/tests/plugins/test_foo.py if it’s an existing plugin where you can verify the build environment and commands used for building insofar as they differ (Go would require you to use go build for example instead of go install).
I’m not going to go into all the details here since this is very plugin-specific but one thing you’ll generally want to add is scenarios and mock is_cross_compiling:

scenarios = [
    ('armv7l', dict(deb_arch='armhf', go_arch='arm')),
    ('aarch64', dict(deb_arch='arm64', go_arch='arm64')),
    ('i386', dict(deb_arch='i386', go_arch='386')),
    ('x86_64', dict(deb_arch='amd64', go_arch='amd64')),
    ('ppc64le', dict(deb_arch='ppc64el', go_arch='ppc64le')),
]

def setUp(self):
    super().setUp()

    self.project_options = snapcraft.ProjectOptions(
        target_deb_arch=self.deb_arch)
    patcher = mock.patch('snapcraft.ProjectOptions.is_cross_compiling')
patcher.start()
self.addCleanup(patcher.stop)

If all went well I hope you’re going to send a PR adding support for the foo plugin - a real one of course, not the fictional one that doesn’t exist. :wink:

4 Likes

This is great, thanks @kalikiana. Do we have plans to run through the existing plug-ins and enable them now?

I’m planning on looking at other plugins. Good candidates might be python and autotools, primarily because snapcraft itself would need them to be able to cross-compile it. Rust was also suggested in a previous discussion because it’s got good support out of the box, like Go. In general I think we just need to see what are the most interesting ones to prioritize.

1 Like

Support for cross-compiling Rust code is on the way. Enablement is a little different to Go because the plugin sets up the toolchain via rustup and uses a Cargo config file to enable it.

Next one up: Autotools!

Next one up: Waf!

Next one up: Python!

Next one up: kbuild!

Is node.js on the list ?

What about the JDK plugin?

Maybe you can resolve the ca-certificates-java problem in the same run:

Would you guys mind opening new topics for the nodejs and jdk plugins? Then we can discuss what’s needed for those to be cross-compiled.

Follow-up on i386 kernels: Cross-compiling an i386 kernel snap

Seems that nodejs still doesn’t cross-compile, do you have idea when it will do it?

1 Like

@kalikiana - Thanks for your help with enabling cross compiling via autotools, etc. I’d like to cross compile with make.py plugin as well. What’s the best approach?

1 Like

Any news on the nodejs cross-compiling?