Turning single executable project into multi-executable

I am trying to create a project for an embedded device that is based on Zephyr ecosystem. The problem I struggle with is that it forces single-executable project. It uses CMake.

The way how Zephyr suggest to create the main CMakeLists.txt (from its documentation):

cmake_minimum_required(VERSION 3.16)

# This is project-dependent
set(ZEPHYR_TOOLCHAIN_VARIANT "gnuarmemb" CACHE STRING "Informs Zephyr about the toolchain used")
set(GNUARMEMB_TOOLCHAIN_PATH ${TOOLCHAIN_PATH} CACHE PATH "Informs Zephyr where the GNU toolchain is located")
set(BOARD nucleo_l432kc CACHE STRING "Board specification for Zephyr")

find_package(Zephyr 2.4.99)

project(MyProject)

# "app" is defined within the call to "find_package(Zephyr ...)".
# This is necessary - to provide at least one source file.
target_sources(app PRIVATE dummy.cpp)

I would like to have more than one executable (for embedded-tests targets and extra executables like long running tests of sensors, etc.) in this environment. I could create multiple projects for that but that is tedious solution, because it decentralizes the build system and makes it really inconvenient in terms of use (defining multiple build directories for CMake, all that manual checking for out-of-date builds that are easy to forget).

Other solution I came into was to use ExternalProject mixed with some CMake code to define an executable target. That kind’a works but it’s very inefficient. The function looked like that:

function(CreateFirmwareExecutableTarget name toolchain_path)

    set(external_proj_name SubBuild_${name})
    set(src_dir ${CMAKE_SOURCE_DIR}/zephyr)
    set(install_dir ${CMAKE_CURRENT_BINARY_DIR}/zephyr_${name})

    include(ExternalProject)
    ExternalProject_Add(${external_proj_name}
        SOURCE_DIR ${src_dir}
        INSTALL_DIR ${install_dir}
        CMAKE_ARGS -DTOOLCHAIN_PATH=${toolchain_path}
                   -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
        BUILD_ALWAYS YES
    )

    add_executable(${name} IMPORTED)
    set_target_properties(${name} PROPERTIES
        IMPORTED_LOCATION ${install_dir}/bin/zephyr.elf)

endfunction()

In the zephyr directory I had then CMakeLists.txt similar to the one in the first listing. That worked, but that is clunky. The Zephyr sources will be build from scratch for each executable. This is very ineffective.

(In the listing above, there is no custom code (like my project’s own code) linked to the executable. That is not yet solved but the solution for that is easy and is not in the scope of this question).

Zephyr doesn’t give any CMake API to create more executables, so I opt for a CMake solution.

One solution which I came up with was to scrape the properties from the app executable (like the static library targets linked to it, sources, link flags, etc.) and create a library target to which targets created with add_executable would link to. That is equivalent to creating a library target counterpart for an executable.

The question is whether am I missing something here? How can I accomplish the task to create a multi-executable project for that environment in non-hacky way using CMake?

This sounds like a feature request to Zephyr at least. I’m sure CMake can help with symptoms of the problem here, but that really sounds like the root cause to me.

That said, I’d try and get the projects to share a single Zephyr build that you make through its own ExternalProject_add call if at all possible. I’m not familiar enough to know if that is viable at all.

@craig.scott Ideas other than that?

1 Like

I would definitely not recommend creating a non-imported target as part of a find_package() call, which you have indicated Zephyr is doing. As you’ve seen, it prevents you from being in control of what target to use, how it should be named and how many such targets you might want to create. Normally, a find_package() command would only create imported targets for things that have already been built.

A much better approach would be for Zephyr to define a command that you can call and you pass it a target that it can then modify the properties of as it needs to. Without knowing the specifics of why Zephyr has chosen to do things the way they have, I’d also recommend you contact them and see if they are willing to modify their project to offer this more conventional method.

For now, if you are stuck with Zephyr as it stands, using ExternalProject similar to the way you already tried would be the route I’d recommend, despite its inefficiencies. If you can use something like ccache, that may help reduce the build times where the same files are being compiled in multiple subprojects. Other alternatives like trying to copy across properties manually between targets are generally things to avoid. You would be assuming there are no other side effects that the find_package() call does, like creating any other auxiliary files, custom targets, generating source files that may have been added to the target, etc.

1 Like