Deploying Qt Projects to Embedded Devices with CMake

When you run a Qt project, QtCreator first deploys the project to a remote embedded Linux device via ssh and then runs the executable on the remote device. This feature enables nearly instant feedback how your Qt application works on the embedded device.

Deployment works fine with the INSTALLS variable of qmake. It does not work out of the box with the install functions of CMake. Fortunately, the Qt trolls provide a workaround. I’ll explain the workaround with an example CMakeLists.txt file.

You want to install the executable of your Qt project in /opt/mycompany/bin on the embedded device, a required third-party library in /opt/mycompany/lib and a directory containing 3D CAD files in /opt/mycompany/cad. The installation section of your CMakeLists.txt, which is located in ${CMAKE_SOURCE_DIR}/src, would look similar to this.

set(CMAKE_INSTALL_PREFIX "/opt/mycompany")
install(TARGETS ${PROJECT_NAME}
        RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
)
install(FILES ./lib/other/libMagic.so 
        DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
)
install(DIRECTORY ./cad/
        DESTINATION ${CMAKE_INSTALL_PREFIX}/cad
)

Local deployment to a Linux PC with make install works fine. Remote deployment to an embedded Linux device doesn’t work.

QtCreator shows the mapping from local file paths to remote file paths in the section Projects > Run Settings > Deployment > Files to deploy. With the install functions above, QtCreator would only show one entry. The executable app is mapped from its build location ${CMAKE_BINARY_DIR}/src/app to src. This is clearly broken.

The idea of the workaround is for CMake to write the mapping from local file paths to remote file paths into a file called QtCreatorDeployment.txt. The correct QtCreatorDeployment.txt would contain the following mapping.

/opt/mycompany
src/../../build-app-Remote_Qt_5_12_1-Release/src/app:bin
src/lib/other/libMagic.so:lib
src/cad/machine1/part1.step:cad/machine1
src/cad/machine1/part3.step:cad/machine1
src/cad/machine2/part6.step:cad/machine2
...

The first line gives the absolute path on the remote device, the install prefix, where the following files are copied. The format for the second and all subsequent lines is

relative/local/file:relative/remote/dir

The local files are relative to ${CMAKE_SOURCE_DIR}, whereas the remote directories are relative to the install prefix /opt/mycompany given in the first line. So, the above line is conceptually equivalent to the remote copy command

$ scp ${CMAKE_SOURCE_DIR}/relative/local/file \
      user@192.168.1.82:/opt/mycompany/relative/remote/dir/file

As usual, the Qt trolls make your life a bit easier and provide two CMake macros to add files and directory trees to QtCreatorDeployment.txt. The first macro add_deployment_file looks as follows:

macro(add_deployment_file SRC DEST)
    file(RELATIVE_PATH path ${CMAKE_SOURCE_DIR} 
         ${CMAKE_CURRENT_SOURCE_DIR})
    file(APPEND "${CMAKE_SOURCE_DIR}/QtCreatorDeployment.txt"
         "${path}/${SRC}:${DEST}\n")
endmacro()

The first file command computes the relative path from the root of the source, ${CMAKE_SOURCE_DIR}, to the source directory currently processed, ${CMAKE_CURRENT_SOURCE_DIR}. The second file command appends the line "${path}/${SRC}:${DEST}\n" to the file QtCreatorDeployment.txt.

For example, the call

add_deployment_file("lib/other/libMagic.so" "lib")

is evaluated as follows:

SRC = "lib/other/libMagic.so", "DEST = lib"
path = "src"
Append: "src/lib/other/libMagic.so:lib\n"

The macro writes the mapping file QtCreatorDeployment.txt to the directory ${CMAKE_SOURCE_DIR}, which is the root of the source tree. All files generated during the build should, however, be written to the build tree. Fortunately, QtCreator looks for the mapping file not only in ${CMAKE_SOURCE_DIR} but also in {CMAKE_BINARY_DIR}, the root of the build tree. Hence, you want to change the second file command to

file(APPEND "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
     "${path}/${SRC}:${DEST}\n")

The second macro add_deployment_directory looks as follows

macro(add_deployment_directory SRC DEST)
    file(GLOB_RECURSE files RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" 
         "${SRC}/*")
    foreach(filename ${files})
        get_filename_component(path ${filename} PATH)
        add_deployment_file("${filename}" "${DEST}/${path}")
    endforeach(filename)
endmacro()

The file command traverses the tree rooted at the directory ${CMAKE_CURRENT_SOURCE_DIR} and stores, in the variable files, all the files that match the globbing expression ${SRC}/*.

The foreach loop iterates over all the files found by the file command. For each file, it calls add_deployment_file with the relative local file path and the remote directory path ${DEST}/${path}, where ${path} is the relative directory path of the local file.

For example, the call

add_deployment_directory("cad" ".")

is evaluated as follows:

SRC = "cad", DEST = "."
files = all file paths relative to "${CMAKE_CURRENT_SOURCE_DIR}" and
                       matching the pattern "cad/*"
files = "cad/machine1/part1.step" "cad/machine1/part3.step"
        "cad/machine2/part6.step"
foreach loop:
  add_deployment_file("cad/machine1/part1.step" "./cad/machine1")
    Append: "src/cad/machine1/part1.step:cad/machine1\n"
  add_deployment_file("cad/machine1/part3.step" "./cad/machine1")
    Append: "src/cad/machine1/part3.step:cad/machine1\n"
  add_deployment_file("cad/machine2/part6.step" "./cad/machine2")
    Append: "src/cad/machine2/part6.step:cad/machine2\n"

Now, you can put all the pieces together and rewrite the installation section of your CMakeLists.txt file as follows.

macro(add_deployment_file SRC DEST)
    file(RELATIVE_PATH path ${CMAKE_SOURCE_DIR} 
         ${CMAKE_CURRENT_SOURCE_DIR})
    file(APPEND "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
         "${path}/${SRC}:${DEST}\n")
endmacro()

macro(add_deployment_directory SRC DEST)
    file(GLOB_RECURSE files RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" 
         "${SRC}/*")
    foreach(filename ${files})
        get_filename_component(path ${filename} PATH)
        add_deployment_file("${filename}" "${DEST}/${path}")
    endforeach(filename)
endmacro()

set(CMAKE_INSTALL_PREFIX "/opt/mycompany")
if(DEPLOYED_REMOTELY)
    # Write base installation path as first line.
    file(WRITE "${CMAKE_BINARY_DIR}/QtCreatorDeployment.txt"
         "${CMAKE_INSTALL_PREFIX}\n")
    # Append mapping for executable.
    file(RELATIVE_PATH relative_exe_path 
         "${CMAKE_CURRENT_SOURCE_DIR}"
         "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}")
    add_deployment_file(relative_exe_path bin)
    # Append mapping for single library file.
    add_deployment_file("lib/other/libMagic.so" "lib")
    # Append all 3D CAD files from local directory "cad".
    add_deployment_directory("cad" ".")
else()
    # The original install commands go here for local deployment.
    ...
endif()

DEPLOYED_REMOTELY is a CMake option, which is defined as

option(DEPLOYED_REMOTELY "Turn on for remote deployment" OFF)

QtCreator shows this option in the section Projects > Build Settings > CMake as a checkbox. If you want to deploy your application remotely, you simply tick the checkbox and press the button Apply Configuration Changes.

When you run the application with Ctrl+R, QtCreator will copy the files to the embedded device according to the mapping in QtCreatorDeployment.txt and run the application on the embedded device.

Scroll to top