Conan: Relocatable toolchain package

Created on 30 Apr 2019  路  16Comments  路  Source: conan-io/conan

I want to create a package encapsulating a toolchain that I can use as build_requires in other packages. The toolchain in question is generated by Yocto as SDK installation script (a shell script). During the installation process of the toolchain the installer relocates the executables to whatever directory I chose to install them in, rewriting all of the shared library RPATHs to fixed absolute paths. It doesn't make sense to package the filesystem snapshot of the toolchain after installation because Conan will install the toolchain dependency to some random directory, breaking the RPATH links and thus making all executables unexecutable ;)

How can I package such a toolchain without breaking it? I see two options:

  • Package the installer itself instead of the installed directory tree and install it during the build_requires resolving phase of the consumer; is that possible somehow with Conan?
  • Do not relocate executables and make Yocto save a relocation script that can be executed later when the packaged directory tree is unpacked to adapt the RPATHs to the actual installation directory; there are options in the SDK installer for that but they are marked as advanced debugging options so I'd rather not touch them. I still need a way to hook into the build_requires installation phase so that I can call the relocation script.
medium medium queue look into whiteboard

Most helpful comment

Hi @jasal82, thanks for sharing your solution.
I have tried exactly the same in the past and wanted to share my current solution here.

Albeit, it is not very far away from yours, the obvious main differences are:

  • I have explicitly set the environment in package_info(), I do very much like your parsing of the "environment-setup" though!
  • I am delivering the installer script as part of the package, as I actually want to distribute binary packages. I would however prefer a relocatable, installed toolchain as part of the package and not the installer script. My first try was exactly what you do in your script, I gave up on this because of the problems you mentioned (packaging of dead links and so on).
  • The actual install is done in deploy(), this works for "conan install xxxx" but not when the package is installed as a transitive dependency of another package (this is actually the main usecase). So I had to insert the workaround in package_info and first check whether an install is needed or not.

Current status:

  • I sort of works
  • I am not very happy with the "solution" as it is not elegant at all
  • I would have hoped this kind of setup is something conan would support much better, as it is not so uncommon
  • Part of the problem is also the Yocto SDK. The relocation script does not seem to be meant used outside the installer. If this would be possible, this would ease the creation of a conan.py somewhat.

I would like the discussion going on, better ideas are needed IMO.
Maybe we can flesh out what we would need from conan to support our usecase much better.

from conans import ConanFile
from conans.errors import ConanException
from conans import tools
import os, stat

from shutil import copyfile


class YoctoInstallerConan(ConanFile):
    name = 'yocto_installer'
    version = "2.6.2"
    description = 'Yocto SDK installer'
    url = '[email protected]:/openembedded.git'
    revision_mode = 'scm'
    scm = { 'type': 'git', 'url': 'auto', 'revision': 'auto' }
    no_copy_source = True

    settings = {
        'os_build' : ['Linux'],       # Host OS
        'arch_build' : ['x86_64'],    # Host Arch
    }

    defs = {
        'installer' : 'poky-glibc-x86_64-image-cortexa9t2hf-neon-toolchain-2.6.1.sh',
        'sysroot' : 'sysroots/cortexa9t2hf-neon-poky-linux-gnueabi',
        'toolsroot' : 'sysroots/x86_64-pokysdk-linux',
        'installer_src' : '/data/oe-core/thud/deploy/sdk/poky-glibc-x86_64-image-cortexa9t2hf-neon-toolchain-{0}.sh'.format(version),
        'system_name' : 'Linux',
        'system_processor' : 'cortexa9t2hf',
        'cflags'   : ' -O2 -pipe -g -feliminate-unused-debug-types ',
        'cxxflags' : ' -O2 -pipe -g -feliminate-unused-debug-types ',
        'ldflags'  : ' -Wl,-O1 -Wl,--hash-style=gnu -Wl,--as-needed ',
        'cppflags' : ''
    }

    def __sdk_installer_run(self):
        exe_file = self.__sdk_installer_bin()
        st = os.stat(exe_file)
        os.chmod(exe_file, st.st_mode | stat.S_IXUSR)
        self.run('{0} -y -d {1}'.format(exe_file, self.__sdk_install_dir()))

    def __sdk_install_dir(self):
        return os.path.join(self.package_folder, 'sdk')

    def __sdk_installer_bin(self):
        return os.path.join(self.package_folder, self.defs['installer'])

    def __check_system(self):
        if self.settings.get_safe('os_build') != 'Linux' or self.settings.get_safe('arch_build') != 'x86_64':
            raise ConanException('Only Linux X86_64 is supported as build platform')

    def __generate_cmake_toolchain_file(self, cmake_toolchain_file_path, toolsroot, sysroot):
        content = '''
        set(CMAKE_SYSTEM_NAME "{system_name}")
        set(CMAKE_SYSTEM_PROCESSOR "{system_processor}")
        set(CMAKE_C_FLAGS "{cflags}" CACHE STRING "" FORCE)
        set(CMAKE_CXX_FLAGS "{cxxflags}"  CACHE STRING "" FORCE)
        set(CMAKE_ASM_FLAGS "{cflags}" CACHE STRING "" FORCE)
        set(CMAKE_LDFLAGS_FLAGS "{cxxflags}" CACHE STRING "" FORCE)
        set(CMAKE_SYSROOT "{sysroot}")
        set(CMAKE_FIND_ROOT_PATH "{sysroot}" "{toolsroot}")
        set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
        set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
        set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
        set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
        '''.format(
            system_name=self.defs['system_name'],
            system_processor=self.defs['system_processor'],
            cflags=self.defs['cflags'],
            cxxflags=self.defs['cxxflags'],
            ldflags=self.defs['ldflags'],
            toolsroot=toolsroot,
            sysroot=sysroot
        )
        file = open(cmake_toolchain_file_path, 'w')
        file.write(content)
        file.close()

    def package(self):
        installer_src = self.defs['installer_src']
        installer_dst = self.__sdk_installer_bin()
        copyfile(installer_src, installer_dst)

    def deploy(self):
        self.__check_system()
        self.__sdk_installer_run()

    def package_info(self):
        # check whether or not deploy is needed
        # workaround, since deploay is only called when: "conan install package-ref"
        # is executed and not when installed as transitive dependency
        if not self.develop and not os.path.exists(self.__sdk_install_dir()):
            print('deploy needed, installing sdk ...')
            self.deploy()

        toolsroot = os.path.join(self.__sdk_install_dir(), self.defs['toolsroot'])
        sysroot = os.path.join(self.__sdk_install_dir(), self.defs['sysroot'])

        self.user_info.TOOLSROOT = toolsroot
        self.user_info.SYSROOT = sysroot

        cmake_toolchain_file_path = os.path.join(self.package_folder, 'toolchain_yocto.cmake')
        self.__generate_cmake_toolchain_file(cmake_toolchain_file_path, toolsroot, sysroot)

        # ---- environment-setup from Yocto exports ----

        self.env_info.SDKTARGETSYSROOT = sysroot

        ## PATH anpassen (self.env_info.PATH.append = prepend to PATH !)
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/sbin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'sbin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/x86_64-pokysdk-linux/bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin/arm-poky-linux-gnueabi'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin/arm-poky-linux-musl'))

        self.env_info.PKG_CONFIG_SYSROOT_DIR = sysroot
        self.env_info.PKG_CONFIG_PATH = [
            os.path.join(sysroot, 'usr/lib/pkgconfig'),
            os.path.join(sysroot, 'usr/share/pkgconfig')
        ]
        self.env_info.CONFIG_SITE = os.path.join(self.__sdk_install_dir(), 'site-config-cortexa9t2hf-neon-poky-linux-gnueabi')

        self.env_info.CC      = 'arm-poky-linux-gnueabi-gcc  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.CXX     = 'arm-poky-linux-gnueabi-g++  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.CPP     = 'arm-poky-linux-gnueabi-gcc -E  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.AS      = 'arm-poky-linux-gnueabi-as '
        self.env_info.LD      = 'arm-poky-linux-gnueabi-ld  --sysroot={0}'.format(sysroot)
        self.env_info.GDB     = 'arm-poky-linux-gnueabi-gdb'
        self.env_info.STRIP   = 'arm-poky-linux-gnueabi-strip'
        self.env_info.RANLIB  = 'arm-poky-linux-gnueabi-ranlib'
        self.env_info.OBJCOPY = 'arm-poky-linux-gnueabi-objcopy'
        self.env_info.OBJDUMP = 'arm-poky-linux-gnueabi-objdump'
        self.env_info.AR      = 'arm-poky-linux-gnueabi-ar'
        self.env_info.NM      = 'arm-poky-linux-gnueabi-nm'
        self.env_info.M4      = 'm4'

        self.env_info.CFLAGS   = self.defs['cflags']
        self.env_info.CXXFLAGS = self.defs['cxxflags']
        self.env_info.LDFLAGS  = self.defs['ldflags']
        self.env_info.CPPFLAGS = self.defs['cppflags']
        self.env_info.KCFLAGS  = ' --sysroot={0}'.format(sysroot)

        self.env_info.TARGET_PREFIX = 'arm-poky-linux-gnueabi-'
        self.env_info.CONFIGURE_FLAGS = '--target=arm-poky-linux-gnueabi --host=arm-poky-linux-gnueabi --build=x86_64-linux --with-libtool-sysroot={0}'.format(sysroot)

        self.env_info.OECORE_NATIVE_SYSROOT = toolsroot
        self.env_info.OECORE_TARGET_SYSROOT = sysroot
        self.env_info.OECORE_ACLOCAL_OPTS   = '-I {0}/usr/share/aclocal'.format(toolsroot)
        self.env_info.OECORE_BASELIB        = 'lib'
        self.env_info.OECORE_TARGET_ARCH    = 'arm'
        self.env_info.OECORE_TARGET_OS      = 'linux-gnueabi'
        self.env_info.OECORE_DISTRO_VERSION = '{0}'.format(self.version)
        self.env_info.OECORE_SDK_VERSION    = '{0}'.format(self.version)
        self.env_info.ARCH = 'arm'
        self.env_info.CROSS_COMPILE = 'arm-poky-linux-gnueabi-'

        # # ---- Conan ----
        self.env_info.CONAN_CMAKE_GENERATOR = 'Ninja'
        # self.env_info.CONAN_CMAKE_TOOLCHAIN_FILE = os.path.join(toolsroot, 'usr/share/cmake/OEToolchainConfig.cmake')
        self.env_info.CONAN_CMAKE_TOOLCHAIN_FILE = cmake_toolchain_file_path
        self.env_info.CONAN_CMAKE_SYSTEM_NAME = self.defs['system_name']
        self.env_info.CONAN_CMAKE_SYSTEM_PROCESSOR = self.defs['system_processor']

        toolsroot_rel = os.path.relpath(toolsroot, self.package_folder)

        self.cpp_info.bindirs = [
            os.path.join(toolsroot_rel, 'bin'),
            os.path.join(toolsroot_rel, 'sbin'),
            os.path.join(toolsroot_rel, 'usr/bin'),
            os.path.join(toolsroot_rel, 'usr/sbin'),
        ]

All 16 comments

Hi @jasal82

Some of the team have been recently using yocto too (ping @danimtb and @lasote). We plan to write down it in docs.

Indeed, RPATHs and absolute paths are a problem, because of relocation of files in other machines. Conan typically relies on not using or discarding RPATHs in libraries and later using runtime LD_LIBRARY_PATH.

For running a custom relocation script in the installation phase, the truth is that Conan doesn't have a specific hook, cause in the design is that Conan packages cannot be changed after installation. Every package contains a manifest with the files and checksum, and one package that is installed can in theory be uploaded to a server. Doing something like changing the files will break the package, the manifest will be outdated, and the package revision (versioning) of the package becomes a nightmare.

(We might need to revise this concept, and analyse the issue again, I am marking this issue for discussion.)

If the package is consumed as build_require, it is not enough to consume its env vars (like LD_LIBRARY_PATH) explicitly, or even implicitly (with virtualrunenv generator)? Does the script do other things besides changing RPATHs?

BTW, there are "hooks" in conan, in which you can perfectly code and inject behavior to packages. Still the problem of breaking the manifest would happen, but if you don't plan to re-upload them, maybe you could build a workaround with a hook. Not a strong recommendation, just an idea, we need first to better understand the problem and possible solutions. Please consider giving a try to setting the environment dynamically in the build_require package, as suggested above.

Hi @jasal82,

Thanks for sharing the question.

I have been working with Yocto's SDKs and currently, I managed to create a "wrapper" recipe to be used as a build require.

Basically, it downloads an SDK installer and extracts it in the package folder. Then, to avoid any hardcoded path, all the information of environment variables and different paths are injected in the recipe side via package_info() --> cpp_info object (https://docs.conan.io/en/latest/reference/conanfile/methods.html#package-info). That way the package can be reused although some scripts would have the absolute extraction paths hardcoded.

Another approach would be to use this kind of wrapper with a build policy "always": https://docs.conan.io/en/latest/mastering/policies.html That way the installer will always be decompressed when it is installed.

It has been a preliminary look into task, but I would like no know more about different use cases and the motivation to have the SDK package with Conan. Any insights about it would be very much appreciated 馃槃

The motivation behind having SDK packages as build requirements is that we want to have a single point of dependency definition. That would make it easier to implement reproducible builds. Our toolchains are built by Yocto and the recipes for them can change so we have to version them. We could build a Docker image containing the toolchain and use that for building the application and library packages but then we'd have to maintain a matrix of Docker images which grows even larger when new toolchain versions emerge. The selection of the Docker image cannot be part of the Conan profile so we'd have to maintain the mapping between profiles and Docker images. It's also more difficult to build locally because a Docker host is required and the correct image must be run. If we could define the toolchains as build requirements we'd be able to install them in a generic Docker container or even without Docker in a VM or a native Linux machine.

I tried something like this:

from conans import ConanFile, tools
import os, glob, re

class MyToolchain(ConanFile):
    name = "toolchain_cortexa7hf"
    version = "0.1"
    settings = "os_build", "arch_build"
    build_policy = "always"

    def build(self):
        url = "https://deagxartifactory.sickcn.net/artifactory/div08-toolchain-ident-vision/lector621/xubuntu/1604/development/oecore-x86_64-cortexa7hf-neon-toolchain-sick0.1-sdk2-20190328.sh"
        tools.download(url, "toolchain.sh", verify=False)
        self.run("chmod +x toolchain.sh")
        command = "./toolchain.sh -y -d %s" % self.package_folder
        self.run(command)

    def package_info(self):
        envSetupFileList = glob.glob(os.path.join(self.package_folder, "environment-setup*"))
        envSetupFile = envSetupFileList[0] if envSetupFileList else None
        if envSetupFile:
            with open(envSetupFile) as f:
                for line in f:
                    match = re.search(r'export ([^=]+)=(.*)', line)
                    if match:
                        setattr(self.env_info, match.group(1), match.group(2))

Currently I'm trying to fix problems with broken symlinks during the packaging phase. The toolchain installer seems to create some invalid symlinks and Conan exits after finding them. There is an environment and config setting to disable broken symlink checking in Conan but I need that defined in the recipe because it's not something that I want to enable globally. The alternative is to remove the broken symlinks programmatically.

The build_policy=always workaround seems like a nice idea but the problem is that I don't want to install a toolchain again if it is already in the cache, e.g. on a developer machine. Ideally I'd like to have a build requirement package which contains the installer script and executes it during dependency installation time, but only if it was not already installed before. I don't see a canonical way of doing that without using the hooks mentioned above.

Would it be possible that you share your wrapper recipe with me?

Hi @jasal82,

Thanks a lot for the feedback and the valuable experience with the Docker image approach.
As you showed, the SDK wrapper recipe I have is pretty much the same one you have posted (yours is even better, as I was hardcoding the export env vars in the package_info()).

We are aware of the broken symlinks and the Yocto investigation was the main reason we introduced skip_broken_symlinks_check config variable.

Currently, there is no way to allow this per-recipe, so my suggestion would be to enable this only for the creation of the toolchain packages (typically done in a CI machine) and upload the SDK as Conan packages (instead of using build_policy=always) for sharing.

Another approach to download every time the SDK (due to build_policy=True), would be downloading it in the source() method and decompress it in the package() method. This will avoid executing the actions in the build() method every time an install command is issued.

I think you are on the right path and it is just a matter of polishing the rough edges.

I fine-tuned my solution a bit and now it seems to be working fine. Had to add some extra code for handling the references in the environment variables and to make Conan append to PATH correctly. Here is what I have:

from conans import ConanFile, tools
import os, glob, re

class ToolchainCortexA7hf(ConanFile):
    name = "toolchain_cortexa7hf"
    version = "0.1"
    settings = "os_build", "arch_build"

    def source(self):
        url = "https://deagxartifactory.sickcn.net/artifactory/div08-toolchain-ident-vision/lector621/xubuntu/1604/development/oecore-x86_64-cortexa7hf-neon-toolchain-sick0.1-sdk2-20190328.sh"
        tools.download(url, "toolchain.sh", verify=False)

    def package(self):
        self.run("chmod +x toolchain.sh")
        command = "./toolchain.sh -y -d %s" % self.package_folder
        self.run(command)

    def package_info(self):
        # We parse the environment-setup script generated by Yocto to obtain all
        # of the environment variables needed for building and append them to
        # the env_info attribute of the toolchain package. That will make Conan
        # apply the environment to all package builds which reference the
        # toolchain package in their build_requires.
        envSetupFileList = glob.glob(os.path.join(self.package_folder, "environment-setup*"))
        envSetupFile = envSetupFileList[0] if envSetupFileList else None
        if envSetupFile:
            # Read all variables into a dictionary first so that we can resolve
            # references later.
            env = {}
            with open(envSetupFile) as f:
                for line in f:
                    match = re.search(r'export ([^=]+)=(.*)', line)
                    if match:
                        key = match.group(1)
                        # Strip possible quotes around the value
                        value = match.group(2).strip("\"")
                        env[key] = value

            # CROSS_COMPILE setting from Yocto env interferes with OpenSSL/crypto build
            # which uses $CROSS_COMPILE$CC as compiler command but Yocto already adds the
            # target prefix to $CC. We just remove it from the environment for now. This
            # should not affect any other builds.
            del env["CROSS_COMPILE"]

            for key, value in env.items():
                # Remove references to self (e.g. in PATH=...:$PATH)
                value = re.sub("\\$%s" % key, "", value)
                # Find all other references and resolve them
                refs = re.findall(r'\$[a-zA-Z0-9_]+', value)
                for ref in refs:
                    value = re.sub("\\%s" % ref, env[ref[1:]], value)
                if key.lower() == "path":
                    # The content of the PATH variable must be set as a list to make
                    # Conan append to the existing PATH. Otherwise it will overwrite
                    # the environment variable.
                    setattr(self.env_info, key, [x for x in value.split(":") if x])
                else:
                    # All other variables can be handled in the standard way.
                    setattr(self.env_info, key, value)

        # Unset command_not_found_handle to fix problems on Ubuntu due to
        # toolchains overriding PYTHONHOME (typing an unknown command in a shell
        # on Ubuntu will run a Python script which will no longer work when a
        # toolchain redefines PYTHONHOME).
        self.env_info.command_not_found_handle = None

As you can see I removed the build_policy = "always". That way Conan seems to be doing the right thing, i.e. running the source() and package() methods only if the package has not already been built before. You still need to pass the --build=toolchain_cortexa7hf to Conan during the install phase if you're using the toolchain package for the first time.

This also requires the broken symlinks fix in the config or environment variable, which I don't like. The point is that the files are already installed in the package folder, so there should be no need to check for broken symlinks. A binary package is never uploaded anywhere in this approach. Please add an option to disable broken symlinks check for a specific recipe instead, that would be a bit cleaner.

Generally I would prefer a dedicated workflow for toolchain packages, maybe one for relocatable ones (which don't require installation) and one for toolchains that require installation on the build host (like this one). You should also think about making toolchains a special build requirement type so that its environment settings could be separated from the rest, e.g. to create a virtualenv activate/deactivate script for these settings only. This would be useful for cases where a separate bootstrap step is needed that must not use the cross-compilation environment, see this other issue for reference.

@jasal82 I noticed you are deleting the CROSS_COMPILE environment variable to fix an issue with OpenSSL. That seems to get me past one build issue with OpenSSL but I then get stuck at another issue where it says:

In file included from armcap.c:8:0:
arm_arch.h:46:6: error: #error "unsupported ARM architecture"
 #    error "unsupported ARM architecture"
      ^~~~~
<builtin>: recipe for target 'armcap.o' failed
make[1]: *** [armcap.o] Error 1
make[1]: Leaving directory '/home/kelannen/.conan/data/OpenSSL/1.0.2r/conan/stable/build/36b9b759adbdc3339846872cd740914337e50db3/openssl-1.0.2r/crypto'
Makefile:287: recipe for target 'build_crypto' failed
make: *** [build_crypto] Error 1
OpenSSL/1.0.2r@conan/stable:
OpenSSL/1.0.2r@conan/stable: ERROR: Package '36b9b759adbdc3339846872cd740914337e50db3' build failed
OpenSSL/1.0.2r@conan/stable: WARN: Build folder /home/kelannen/.conan/data/OpenSSL/1.0.2r/conan/stable/build/36b9b759adbdc3339846872cd740914337e50db3
ERROR: OpenSSL/1.0.2r@conan/stable: Error in build() method, line 109
        self.unix_build(config_options_string)
while calling 'unix_build', line 210
        self.run_in_src("make", show_output=True)
while calling 'run_in_src', line 131
        self.run(command)
        ConanException: Error 512 while executing make

This is for armv8 architecture.
Have you been able to successfully build OpenSSL?

@kevswims I was able to build OpenSSL successfully after disabling the assembly routines by setting

def configure(self):
    self.options["OpenSSL"].no_asm = True

from the consumer's conanfile. That will make it use the slower C implementations instead but at least it compiles.

Hi @jasal82, thanks for sharing your solution.
I have tried exactly the same in the past and wanted to share my current solution here.

Albeit, it is not very far away from yours, the obvious main differences are:

  • I have explicitly set the environment in package_info(), I do very much like your parsing of the "environment-setup" though!
  • I am delivering the installer script as part of the package, as I actually want to distribute binary packages. I would however prefer a relocatable, installed toolchain as part of the package and not the installer script. My first try was exactly what you do in your script, I gave up on this because of the problems you mentioned (packaging of dead links and so on).
  • The actual install is done in deploy(), this works for "conan install xxxx" but not when the package is installed as a transitive dependency of another package (this is actually the main usecase). So I had to insert the workaround in package_info and first check whether an install is needed or not.

Current status:

  • I sort of works
  • I am not very happy with the "solution" as it is not elegant at all
  • I would have hoped this kind of setup is something conan would support much better, as it is not so uncommon
  • Part of the problem is also the Yocto SDK. The relocation script does not seem to be meant used outside the installer. If this would be possible, this would ease the creation of a conan.py somewhat.

I would like the discussion going on, better ideas are needed IMO.
Maybe we can flesh out what we would need from conan to support our usecase much better.

from conans import ConanFile
from conans.errors import ConanException
from conans import tools
import os, stat

from shutil import copyfile


class YoctoInstallerConan(ConanFile):
    name = 'yocto_installer'
    version = "2.6.2"
    description = 'Yocto SDK installer'
    url = '[email protected]:/openembedded.git'
    revision_mode = 'scm'
    scm = { 'type': 'git', 'url': 'auto', 'revision': 'auto' }
    no_copy_source = True

    settings = {
        'os_build' : ['Linux'],       # Host OS
        'arch_build' : ['x86_64'],    # Host Arch
    }

    defs = {
        'installer' : 'poky-glibc-x86_64-image-cortexa9t2hf-neon-toolchain-2.6.1.sh',
        'sysroot' : 'sysroots/cortexa9t2hf-neon-poky-linux-gnueabi',
        'toolsroot' : 'sysroots/x86_64-pokysdk-linux',
        'installer_src' : '/data/oe-core/thud/deploy/sdk/poky-glibc-x86_64-image-cortexa9t2hf-neon-toolchain-{0}.sh'.format(version),
        'system_name' : 'Linux',
        'system_processor' : 'cortexa9t2hf',
        'cflags'   : ' -O2 -pipe -g -feliminate-unused-debug-types ',
        'cxxflags' : ' -O2 -pipe -g -feliminate-unused-debug-types ',
        'ldflags'  : ' -Wl,-O1 -Wl,--hash-style=gnu -Wl,--as-needed ',
        'cppflags' : ''
    }

    def __sdk_installer_run(self):
        exe_file = self.__sdk_installer_bin()
        st = os.stat(exe_file)
        os.chmod(exe_file, st.st_mode | stat.S_IXUSR)
        self.run('{0} -y -d {1}'.format(exe_file, self.__sdk_install_dir()))

    def __sdk_install_dir(self):
        return os.path.join(self.package_folder, 'sdk')

    def __sdk_installer_bin(self):
        return os.path.join(self.package_folder, self.defs['installer'])

    def __check_system(self):
        if self.settings.get_safe('os_build') != 'Linux' or self.settings.get_safe('arch_build') != 'x86_64':
            raise ConanException('Only Linux X86_64 is supported as build platform')

    def __generate_cmake_toolchain_file(self, cmake_toolchain_file_path, toolsroot, sysroot):
        content = '''
        set(CMAKE_SYSTEM_NAME "{system_name}")
        set(CMAKE_SYSTEM_PROCESSOR "{system_processor}")
        set(CMAKE_C_FLAGS "{cflags}" CACHE STRING "" FORCE)
        set(CMAKE_CXX_FLAGS "{cxxflags}"  CACHE STRING "" FORCE)
        set(CMAKE_ASM_FLAGS "{cflags}" CACHE STRING "" FORCE)
        set(CMAKE_LDFLAGS_FLAGS "{cxxflags}" CACHE STRING "" FORCE)
        set(CMAKE_SYSROOT "{sysroot}")
        set(CMAKE_FIND_ROOT_PATH "{sysroot}" "{toolsroot}")
        set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
        set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
        set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
        set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
        '''.format(
            system_name=self.defs['system_name'],
            system_processor=self.defs['system_processor'],
            cflags=self.defs['cflags'],
            cxxflags=self.defs['cxxflags'],
            ldflags=self.defs['ldflags'],
            toolsroot=toolsroot,
            sysroot=sysroot
        )
        file = open(cmake_toolchain_file_path, 'w')
        file.write(content)
        file.close()

    def package(self):
        installer_src = self.defs['installer_src']
        installer_dst = self.__sdk_installer_bin()
        copyfile(installer_src, installer_dst)

    def deploy(self):
        self.__check_system()
        self.__sdk_installer_run()

    def package_info(self):
        # check whether or not deploy is needed
        # workaround, since deploay is only called when: "conan install package-ref"
        # is executed and not when installed as transitive dependency
        if not self.develop and not os.path.exists(self.__sdk_install_dir()):
            print('deploy needed, installing sdk ...')
            self.deploy()

        toolsroot = os.path.join(self.__sdk_install_dir(), self.defs['toolsroot'])
        sysroot = os.path.join(self.__sdk_install_dir(), self.defs['sysroot'])

        self.user_info.TOOLSROOT = toolsroot
        self.user_info.SYSROOT = sysroot

        cmake_toolchain_file_path = os.path.join(self.package_folder, 'toolchain_yocto.cmake')
        self.__generate_cmake_toolchain_file(cmake_toolchain_file_path, toolsroot, sysroot)

        # ---- environment-setup from Yocto exports ----

        self.env_info.SDKTARGETSYSROOT = sysroot

        ## PATH anpassen (self.env_info.PATH.append = prepend to PATH !)
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/sbin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'sbin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/x86_64-pokysdk-linux/bin'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin/arm-poky-linux-gnueabi'))
        self.env_info.PATH.append(os.path.join(toolsroot, 'usr/bin/arm-poky-linux-musl'))

        self.env_info.PKG_CONFIG_SYSROOT_DIR = sysroot
        self.env_info.PKG_CONFIG_PATH = [
            os.path.join(sysroot, 'usr/lib/pkgconfig'),
            os.path.join(sysroot, 'usr/share/pkgconfig')
        ]
        self.env_info.CONFIG_SITE = os.path.join(self.__sdk_install_dir(), 'site-config-cortexa9t2hf-neon-poky-linux-gnueabi')

        self.env_info.CC      = 'arm-poky-linux-gnueabi-gcc  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.CXX     = 'arm-poky-linux-gnueabi-g++  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.CPP     = 'arm-poky-linux-gnueabi-gcc -E  -march=armv7-a -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 --sysroot={0}'.format(sysroot)
        self.env_info.AS      = 'arm-poky-linux-gnueabi-as '
        self.env_info.LD      = 'arm-poky-linux-gnueabi-ld  --sysroot={0}'.format(sysroot)
        self.env_info.GDB     = 'arm-poky-linux-gnueabi-gdb'
        self.env_info.STRIP   = 'arm-poky-linux-gnueabi-strip'
        self.env_info.RANLIB  = 'arm-poky-linux-gnueabi-ranlib'
        self.env_info.OBJCOPY = 'arm-poky-linux-gnueabi-objcopy'
        self.env_info.OBJDUMP = 'arm-poky-linux-gnueabi-objdump'
        self.env_info.AR      = 'arm-poky-linux-gnueabi-ar'
        self.env_info.NM      = 'arm-poky-linux-gnueabi-nm'
        self.env_info.M4      = 'm4'

        self.env_info.CFLAGS   = self.defs['cflags']
        self.env_info.CXXFLAGS = self.defs['cxxflags']
        self.env_info.LDFLAGS  = self.defs['ldflags']
        self.env_info.CPPFLAGS = self.defs['cppflags']
        self.env_info.KCFLAGS  = ' --sysroot={0}'.format(sysroot)

        self.env_info.TARGET_PREFIX = 'arm-poky-linux-gnueabi-'
        self.env_info.CONFIGURE_FLAGS = '--target=arm-poky-linux-gnueabi --host=arm-poky-linux-gnueabi --build=x86_64-linux --with-libtool-sysroot={0}'.format(sysroot)

        self.env_info.OECORE_NATIVE_SYSROOT = toolsroot
        self.env_info.OECORE_TARGET_SYSROOT = sysroot
        self.env_info.OECORE_ACLOCAL_OPTS   = '-I {0}/usr/share/aclocal'.format(toolsroot)
        self.env_info.OECORE_BASELIB        = 'lib'
        self.env_info.OECORE_TARGET_ARCH    = 'arm'
        self.env_info.OECORE_TARGET_OS      = 'linux-gnueabi'
        self.env_info.OECORE_DISTRO_VERSION = '{0}'.format(self.version)
        self.env_info.OECORE_SDK_VERSION    = '{0}'.format(self.version)
        self.env_info.ARCH = 'arm'
        self.env_info.CROSS_COMPILE = 'arm-poky-linux-gnueabi-'

        # # ---- Conan ----
        self.env_info.CONAN_CMAKE_GENERATOR = 'Ninja'
        # self.env_info.CONAN_CMAKE_TOOLCHAIN_FILE = os.path.join(toolsroot, 'usr/share/cmake/OEToolchainConfig.cmake')
        self.env_info.CONAN_CMAKE_TOOLCHAIN_FILE = cmake_toolchain_file_path
        self.env_info.CONAN_CMAKE_SYSTEM_NAME = self.defs['system_name']
        self.env_info.CONAN_CMAKE_SYSTEM_PROCESSOR = self.defs['system_processor']

        toolsroot_rel = os.path.relpath(toolsroot, self.package_folder)

        self.cpp_info.bindirs = [
            os.path.join(toolsroot_rel, 'bin'),
            os.path.join(toolsroot_rel, 'sbin'),
            os.path.join(toolsroot_rel, 'usr/bin'),
            os.path.join(toolsroot_rel, 'usr/sbin'),
        ]

Hi @mbodmer, that's also a very interesting solution. When I started working on the toolchain packaging the deploy() function was not there yet or I missed it. A few weeks ago I evaluated it and tried to use deploy for installing the toolchain packages. I eventually gave up because of the issue you mentioned (not working in transitive case). Didn't think about calling it from package_info() so thank you very much for sharing this!

We are still using the solution I posted above and it works reasonably well. You have to be careful though not to upload binary packages of the toolchains to the remotes or you end up with user clients downloading toolchain packages with hardcoded absolute paths of someone else's machine or the buildserver.

Any progress on this? I think what we would need is something like an install() method that is called when a binary package is retrieved from a remote. The default implementation would unpack and copy the files into the package folder but we could override it to instead run the installer script for example. A post_install() would be fine as well.

Or as a minimum an option to let the package always be built when not in local cache. That could be an addition to the existing build policies.

We are still using the solution I posted above and it works reasonably well. You have to be careful though not to upload binary packages of the toolchains to the remotes or you end up with user clients downloading toolchain packages with hardcoded absolute paths of someone else's machine or the buildserver.

I uploaded the toolchain recipes to a different Conan repository where ordinary users have no write permissions. That solves the problem temporarily.

Does anyone have tips on how to actually use the toolchain package? I'm experienced with Yocto but not with Conan (yet), and I can't figure it out.

I'm using the conanfile.py from https://github.com/conan-io/conan/issues/5059#issuecomment-490006689, with my own SDK. (I want to try out each approach described here so I understand the problems). Build like this:

CONAN_SKIP_BROKEN_SYMLINKS_CHECK=True conan create . demo/testing

Then, as a cross-compilation test I created a simple main.cpp that just prints a message on std::out.

conanfile.txt:

[build_requires]
toolchain_cortexa9t2hf/0.1@demo/testing

[generators]
cmake

I created a profile at ~/.conan/profiles/cross:

[build_requires]
toolchain_cortexa9t2hf/0.1@demo/testing

[settings]
os=Linux
os_build=Linux
arch=armv7hf

And attempted to build my demo app like this:

mkdir build && cd build
CONAN_SKIP_BROKEN_SYMLINKS_CHECK=True conan install .. --profile=cross --settings arch_build=armv7hf
cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build .

But the executable is built for my host architecture.

I feel like I'm close but just missing something small. In the conanbuildinfo.txt that gets generated in the demo app 'build' directory, I can see that the environment variables from the toolchain package are getting in. But I guess they're just not being used:

[USER_toolchain_cortexa9t2hf]
[ENV_toolchain_cortexa9t2hf]
AR=arm-linux-gnueabi-ar
ARCH=arm
AS="arm-linux-gnueabi-as "
CC="arm-linux-gnueabi-gcc  -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -fstack-protector-strong  -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security -Werror=format-security --sysroot=$SDKTARGETSYSROOT"
... 

@chris-laplante You need to put some more information in your host (e.g. target) profile. Mine is:

[build_requires]
dynniq_yocto_sdk_cortexa9hf/3.0.0@dynniq/stable
cmake/3.17.3

[settings]
os=Linux
os_build=Linux
arch=armv7
arch_build=x86_64
compiler=gcc
compiler.version=9
compiler.libcxx=libstdc++11
build_type=Release
cppstd=17

Then create your demo app package with command:
conan create .. <username>/<channel> --profile:host=cross --profile:build=default

For your information, I include my current SDK conan file below. To force a "rebuild" when other developers install the SDK package out of Artifactory, I don't upload the binary package, only the recipe + SDK intaller:
conan upload dynniq_yocto_sdk_cortexa9hf/3.0.0@<username>/stable -r <artifactory_remote>

from conans import ConanFile
from conans import ConanFile, tools
import os, glob, re, shutil, stat

class DynniqSdkCortexA9hf(ConanFile):
    name = "dynniq_yocto_sdk_cortexa9hf"
    version = "3.0.0"
    settings = { "os_build": ["Linux"], "arch_build": ["x86_64"] }
    targetarch = "cortexa9hf-neon" if version[0] in ('1', '2') else 'cortexa9t2hf-neon'
    sdk_file = 'dynniq-yoctosdk-x86_64-' + targetarch + '-toolchain-' + version + '.sh'
    exports_sources = [sdk_file]


    def package(self):
        sdkpath = os.path.join(self.source_folder, self.sdk_file)
        os.chmod(sdkpath, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IXUSR | stat.S_IXGRP)
        command = sdkpath + " -y -d %s" % self.package_folder
        self.run(command)
        sudo_exe = os.path.join(self.package_folder, 'sysroots', self.targetarch + '-poky-linux-gnueabi/usr/bin/sudo')
        os.chmod(sudo_exe, stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)

    def package_info(self):
        # We parse the environment-setup script generated by Yocto to obtain all
        # of the environment variables needed for building and append them to
        # the env_info attribute of the toolchain package. That will make Conan
        # apply the environment to all package builds which reference the
        # toolchain package in their build_requires.
        envSetupFileList = glob.glob(os.path.join(self.package_folder, "environment-setup*"))
        envSetupFile = envSetupFileList[0] if envSetupFileList else None
        if envSetupFile:
            # Read all variables into a dictionary first so that we can resolve
            # references later.
            env = {}
            with open(envSetupFile) as f:
                for line in f:
                    match = re.search(r'export ([^=]+)=(.*)', line)
                    if match:
                        key = match.group(1)
                        # Strip possible quotes around the value
                        value = match.group(2).strip("\"")
                        env[key] = value

            # CROSS_COMPILE setting from Yocto env interferes with OpenSSL/crypto build
            # which uses $CROSS_COMPILE$CC as compiler command but Yocto already adds the
            # target prefix to $CC. We just remove it from the environment for now. This
            # should not affect any other builds.
            del env["CROSS_COMPILE"]

            for key, value in env.items():
                # Remove references to self (e.g. in PATH=...:$PATH)
                value = re.sub("\\$%s" % key, "", value)
                # Find all other references and resolve them
                refs = re.findall(r'\$[a-zA-Z0-9_]+', value)
                for ref in refs:
                    value = re.sub("\\%s" % ref, env[ref[1:]], value)
                if key.lower() == "path":
                    # The content of the PATH variable must be set as a list to make
                    # Conan append to the existing PATH. Otherwise it will overwrite
                    # the environment variable.
                    setattr(self.env_info, key, [x for x in value.split(":") if x])
                else:
                    # All other variables can be handled in the standard way.
                    setattr(self.env_info, key, value)

        # Unset command_not_found_handle to fix problems on Ubuntu due to
        # toolchains overriding PYTHONHOME (typing an unknown command in a shell
        # on Ubuntu will run a Python script which will no longer work when a
        # toolchain redefines PYTHONHOME).
        self.env_info.command_not_found_handle = None
        toolchain = os.path.join(self.package_folder, "sysroots", "x86_64-pokysdk-linux", "usr", "share", "cmake", "OEToolchainConfig.cmake")
        setattr(self.env_info, "CONAN_CMAKE_TOOLCHAIN_FILE", toolchain)

@wdobbe Thanks to your clarifications I was able to build my application.
For some reason I wasn't able to get 'conan create' working in the test app, but these two commands did work:

conan install . --profile:host=cross --profile:build=default --install-folder build
conan build . --build-folder build

Now I'm going to work on figuring out the remaining roadblocks for a relocatable SDK.

Thanks again!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

uilianries picture uilianries  路  3Comments

uilianries picture uilianries  路  3Comments

zomeck picture zomeck  路  3Comments

mpdelbuono picture mpdelbuono  路  3Comments

niosHD picture niosHD  路  3Comments