Cross Compiling With CMake¶
Cross-compiling a piece of software means that the software is built on one system, but is intended to run on a different system. The system used to build the software will be called the “build host,” and the system for which the software is built will be called the “target system” or “target platform.” The target system usually runs a different operating system (or none at all) and/or runs on different hardware. A typical use case is in software development for embedded devices like network switches, mobile phones, or engine control units. In these cases, the target platform doesn’t have or is not able to run the required software development environment.
Cross-compiling is fully supported by CMake, ranging from cross-compiling from Linux to Windows; cross-compiling for supercomputers, through to cross-compiling for small embedded devices without an operating system (OS).
Cross-compiling has several consequences for CMake:
CMake cannot automatically detect the target platform.
CMake cannot find libraries and headers in the default system directories.
Executables built during cross compiling cannot be executed.
Cross-compiling support doesn’t mean that all CMake-based projects can be magically cross-compiled out-of-the-box (some are), but that CMake separates between information about the build platform and target platform and gives the user mechanisms to solve cross-compiling issues without additional requirements such as running virtual machines, etc.
To support cross-compiling for a specific software project, CMake must to be told about the target platform via a toolchain file. The CMakeLists.txt may have to be adjusted. It is aware that the build platform may have different properties than the target platform, and it has to deal with the instances where a compiled executable tries to execute on the build host.
Toolchain Files¶
In order to use CMake for cross-compiling, a CMake file that describes the target platform has to be created, called the “toolchain file,” This file tells CMake everything it needs to know about the target platform. Here is an example that uses the MinGW cross-compiler for Windows under Linux; the contents will be explained line-by-line afterwards.
# the name of the target operating system
set(CMAKE_SYSTEM_NAME Windows)
# which compilers to use for C and C++
set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
# where is the target environment located
set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc
/home/alex/mingw-install)
# adjust the default behavior of the FIND_XXX() commands:
# search programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# search headers and libraries in the target environment
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
Assuming that this file is saved with the name TC-mingw.cmake in your
home directory, you instruct CMake to use this file by setting the
CMAKE_TOOLCHAIN_FILE
variable:
~/src$ cd build
~/src/build$ cmake -DCMAKE_TOOLCHAIN_FILE=~/TC-mingw.cmake ..
...
CMAKE_TOOLCHAIN_FILE
has to be specified only on the initial CMake
run; after that, the results are reused from the CMake cache. You
don’t need to write a separate toolchain file for every piece of
software you want to build. The toolchain files are per target
platform; i.e. if you are building several software packages for the
same target platform, you only have to write one toolchain file that
can be used for all packages.
What do the settings in the toolchain file mean? We will examine them one-by-one. Since CMake cannot guess the target operating system or hardware, you have to set the following CMake variables:
CMAKE_SYSTEM_NAME
This variable is mandatory; it sets the name of the target system, i.e. to the same value as
CMAKE_SYSTEM_NAME
would have if CMake were run on the target system. Typical examples are “Linux” and “Windows.” It is used for constructing the file names of the platform files like Linux.cmake or Windows-gcc.cmake. If your target is an embedded system without an OS, setCMAKE_SYSTEM_NAME
to “Generic.” PresettingCMAKE_SYSTEM_NAME
this way instead of being detected, automatically causes CMake to consider the build a cross-compiling build and the CMake variableCMAKE_CROSSCOMPILING
will be set to TRUE.CMAKE_CROSSCOMPILING
is the variable that should be tested in CMake files to determine whether the current build is a cross-compiled build or not.CMAKE_SYSTEM_VERSION
Sets the version of your target system.
CMAKE_SYSTEM_PROCESSOR
This variable is optional; it sets the processor or hardware name of the target system. It is used in CMake for one purpose, to load the
${CMAKE_SYSTEM_NAME}-COMPILER_ID-${CMAKE_SYSTEM_PROCESSOR}.cmake
file. This file can be used to modify settings such as compiler flags for the target. You should only have to set this variable if you are using a cross-compiler where each target needs special build settings. The value can be chosen freely, so it could be, for example, i386, IntelPXA255, or MyControlBoardRev42.
In CMake code, the CMAKE_SYSTEM_XXX
variables always describe the
target platform. The same is true for the short WIN32
,
UNIX
, APPLE
variables. These variables can be used
to test the properties of the target. If it is necessary to test the build
host system, there is a corresponding set of variables:
CMAKE_HOST_SYSTEM
, CMAKE_HOST_SYSTEM_NAME
,
CMAKE_HOST_SYSTEM_VERSION
,
CMAKE_HOST_SYSTEM_PROCESSOR
; and also the short forms:
CMAKE_HOST_WIN32
, CMAKE_HOST_UNIX
and CMAKE_HOST_APPLE
.
Since CMake cannot guess the target system, it cannot guess which compiler it should use. Setting the following variables defines what compilers to use for the target system.
CMAKE_C_COMPILER
This specifies the C compiler executable as either a full path or just the filename. If it is specified with full path, then this path will be preferred when searching for the C++ compiler and the other tools (binutils, linker, etc.). If the compiler is a GNU cross-compiler with a prefixed name (e.g. “arm-elf-gcc”), CMake will detect this and automatically find the corresponding C++ compiler (i.e. “arm-elf-c++”). The compiler can also be set via the
CC
environment variable. SettingCMAKE_C_COMPILER
directly in a toolchain file has the advantage that the information about the target system is completely contained in this file, and it does not depend on environment variables.CMAKE_CXX_COMPILER
This specifies the C++ compiler executable as either a full path or just the filename. It is handled the same way as
CMAKE_C_COMPILER
. If the toolchain is a GNU toolchain, it should suffice to set onlyCMAKE_C_COMPILER
; CMake should find the corresponding C++ compiler automatically. As forCMAKE_C_COMPILER
, also for C++ the compiler can be set via theCXX
environment variable.
Finding External Libraries, Programs and Other Files¶
Most non-trivial projects make use of external libraries or
tools. CMake offers the find_program
, find_library
,
find_file
, find_path
, and find_package
commands for this purpose. They search the file system in common places for
these files and return the results. find_package
is a bit
different in that it does not actually search itself, but executes
Find<*>.cmake
modules, which in turn call
the find_program
, find_library
, find_file
,
and find_path
commands.
When cross-compiling, these commands become more complicated. For
example, when cross-compiling to Windows on a Linux system, getting
/usr/lib/libjpeg.so`
as the result of the command
find_package(JPEG)
would be useless, since this would be the JPEG
library for the host system and not the target system. In some cases, you
want to find files that are meant for the target platform; in other cases
you will want to find files for the build host. The following variables are
designed to give you the flexibility to change how the typical find
commands in CMake work, so that you can find both build host and
target files as necessary.
The toolchain will come with its own set of libraries and headers for the target platform, which are usually installed under a common prefix. It is a good idea to set up a directory where all the software that is built for the target platform will be installed, so that the software packages don’t get mixed up with the libraries that come with the toolchain.
The find_program
command is typically used to find a program that
will be executed during the build, so this should still search in the host
file system, and not in the environment of the target
platform. find_library
is normally used to find a library that is
then used for linking purposes, so this command should only search in the
target environment. For find_path
and find_file
, it is
not so obvious; in many cases, they are used to search for headers, so by
default they should only search in the target environment. The following CMake
variables can be set to adjust the behavior of the find commands for
cross-compiling.
CMAKE_FIND_ROOT_PATH
This is a list of the directories that contain the target environment. Each of the directories listed here will be prepended to each of the search directories of every find command. Assuming your target environment is installed under
/opt/eldk/ppc_74xx
and your installation for that target platform goes to~/install-eldk-ppc74xx
, setCMAKE_FIND_ROOT_PATH
to these two directories. Thenfind_library(JPEG_LIB jpeg)
will search in/opt/eldk/ppc_74xx/lib
,/opt/eldk/ppc_74xx/usr/lib
,~/install-eldk-ppc74xx/lib
,~/install-eldk-ppc74xx/usr/lib
, and should result in/opt/eldk/ppc_74xx/usr/lib/libjpeg.so
.By default,
CMAKE_FIND_ROOT_PATH
is empty. If set, first the directories prefixed with the path given inCMAKE_FIND_ROOT_PATH
will be searched, and then the unprefixed versions of the same directories will be searched.By setting this variable, you are basically adding a new set of search prefixes to all of the find commands in CMake, but for some find commands you may not want to search the target or host directories. You can control how each find command invocation works by passing in one of the three following options
NO_CMAKE_FIND_ROOT_PATH
,ONLY_CMAKE_FIND_ROOT_PATH
, orCMAKE_FIND_ROOT_PATH_BOTH
when you call it. You can also control how the find commands work using the following three variables.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM
This sets the default behavior for the
find_program
command. It can be set to NEVER, ONLY, or BOTH. When set to NEVER,CMAKE_FIND_ROOT_PATH
will not be used forfind_program
calls except where it is enabled explicitly. If set to ONLY, only the search directories with the prefixes coming fromCMAKE_FIND_ROOT_PATH
will be used byfind_program
. The default is BOTH, which means that first the prefixed directories and then the unprefixed directories, will be searched.In most cases,
find_program
is used to search for an executable which will then be executed, e.g. usingexecute_process
oradd_custom_command
. So in most cases an executable from the build host is required, so settingCMAKE_FIND_ROOT_PATH_MODE_PROGRAM
to NEVER is normally preferred.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY
This is the same as above, but for the
find_library
command. In most cases this is used to find a library which will then be used for linking, so a library for the target is required. In most cases, it should be set to ONLY.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE
This is the same as above and used for both
find_path
andfind_file
. In most cases, this is used for finding include directories, so the target environment should be searched. In most cases, it should be set to ONLY. If you also need to find files in the file system of the build host (e.g. some data files that will be processed during the build); you may need to adjust the behavior for thosefind_path
orfind_file
calls using theNO_CMAKE_FIND_ROOT_PATH
,ONLY_CMAKE_FIND_ROOT_PATH
andCMAKE_FIND_ROOT_PATH_BOTH
options.
With a toolchain file set up as described, CMake now knows how to handle the target platform and the cross-compiler. We should now able to build software for the target platform. For complex projects, there are more issues that must to be taken care of.
System Inspection¶
Most portable software projects have a set of system inspection tests
for determining the properties of the (target) system. The simplest
way to check for a system feature with CMake is by testing
variables. For this purpose, CMake provides the variables
UNIX
, WIN32
, and APPLE
. When
cross-compiling, these variables apply to the target platform, for
testing the build host platform there are corresponding
variables CMAKE_HOST_UNIX
, CMAKE_HOST_WIN32
,
and CMAKE_HOST_APPLE
.
If this granularity is too coarse, the variables
CMAKE_SYSTEM_NAME
, CMAKE_SYSTEM
,
CMAKE_SYSTEM_VERSION
, and
CMAKE_SYSTEM_PROCESSOR
can be tested, along with their
counterparts CMAKE_HOST_SYSTEM_NAME
,
CMAKE_HOST_SYSTEM
, CMAKE_HOST_SYSTEM_VERSION
,
and CMAKE_HOST_SYSTEM_PROCESSOR
, which contain the same
information, but for the build host and not for the target system.
if(CMAKE_SYSTEM MATCHES Windows)
message(STATUS "Target system is Windows")
endif()
if(CMAKE_HOST_SYSTEM MATCHES Linux)
message(STATUS "Build host runs Linux")
endif()
Using Compile Checks¶
In CMake, there are macros such as
check_include_files
and
check_c_source_runs
that are used to test
the properties of the platform. Most of these macros internally use
either the try_compile
or the try_run
commands.
The try_compile
command works as expected when
cross-compiling; it tries to compile the piece of code with the
cross-compiling toolchain, which will give the expected result.
All tests using try_run
will not work since the created
executables cannot normally run on the build host. In some cases, this
might be possible (e.g. using virtual machines, emulation layers like
Wine or interfaces to the actual target) as CMake does not depend on
such mechanisms. Depending on emulators during the build process would
introduce a new set of potential problems; they may have a different
view on the file system, use other line endings, require special
hardware or software, etc.
If try_run
is invoked when cross-compiling, it will first
try to compile the software, which will work the same way as when not
cross compiling. If this succeeds, it will check the variable
CMAKE_CROSSCOMPILING
to determine whether the resulting
executable can be executed or not. If it cannot, it will create two
cache variables, which then have to be set by the user or via the
CMake cache. Assume the command looks like this:
try_run(SHARED_LIBRARY_PATH_TYPE
SHARED_LIBRARY_PATH_INFO_COMPILED
${PROJECT_BINARY_DIR}/CMakeTmp
${PROJECT_SOURCE_DIR}/CMake/SharedLibraryPathInfo.cxx
OUTPUT_VARIABLE OUTPUT
ARGS "LDPATH"
)
In this example, the source file SharedLibraryPathInfo.cxx
will be
compiled and if that succeeds, the resulting executable should be
executed. The variable SHARED_LIBRARY_PATH_INFO_COMPILED
will be set
to the result of the build, i.e. TRUE or FALSE. CMake will create a
cache variable SHARED_LIBRARY_PATH_TYPE
and preset it to
PLEASE_FILL_OUT-FAILED_TO_RUN
. This variable must be set to what the
exit code of the executable would have been if it had been executed on
the target. Additionally, CMake will create a cache variable
SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
and preset it to
PLEASE_FILL_OUT-NOTFOUND
. This variable should be set to the output
that the executable prints to stdout and stderr if it were executed on
the target. This variable is only created if the try_run
command was used with the RUN_OUTPUT_VARIABLE
or the
OUTPUT_VARIABLE
argument. You have to fill in the appropriate
values for these variables. To help you with this CMake tries its best
to give you useful information. To accomplish this CMake creates a file
${CMAKE_BINARY_DIR}/TryRunResults.cmake
, which you can see an
example of here:
# SHARED_LIBRARY_PATH_TYPE
# indicates whether the executable would have been able to run
# on its target platform. If so, set SHARED_LIBRARY_PATH_TYPE
# to the exit code (in many cases 0 for success), otherwise
# enter "FAILED_TO_RUN".
# SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
# contains the text the executable would have printed on
# stdout and stderr. If the executable would not have been
# able to run, set SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
# empty. Otherwise check if the output is evaluated by the
# calling CMake code. If so, check what the source file would
# have printed when called with the given arguments.
# The SHARED_LIBRARY_PATH_INFO_COMPILED variable holds the build
# result for this TRY_RUN().
#
# Source file: ~/src/SharedLibraryPathInfo.cxx
# Executable : ~/build/cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE
# Run arguments: LDPATH
# Called from: [1] ~/src/CMakeLists.cmake
set(SHARED_LIBRARY_PATH_TYPE
"0"
CACHE STRING "Result from TRY_RUN" FORCE)
set(SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
""
CACHE STRING "Output from TRY_RUN" FORCE)
You can find all of the variables that CMake could not determine, from
which CMake file they were called, the source file, the arguments for
the executable, and the path to the executable. CMake will also copy
the executables to the build directory; they have the names
cmTryCompileExec-<name of the variable>
, e.g. in this case
cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE
. You can then try to run
this executable manually on the actual target platform and check the
results.
Once you have these results, they have to be put into the CMake cache. This can be done by using ccmake or cmake-gui. and editing the variables directly in the cache. It is not possible to reuse these changes in another build directory or if CMakeCache.txt is removed.
The recommended approach is to use the TryRunResults.cmake file created by CMake. You should copy it to a safe location (i.e. where it will not be removed if the build directory is deleted) and give it a useful name, e.g. TryRunResults-MyProject-eldk-ppc.cmake. The contents of this file have to be edited so that the set commands set the required variables to the appropriate values for the target system. This file can then be used to preload the CMake cache by using the -C option of cmake:
src/build/ $ cmake -C ~/TryRunResults-MyProject-eldk-ppc.cmake .
You do not have to use the other CMake options again as they are now in the cache. This way you can use MyProjectTryRunResults-eldk-ppc.cmake in multiple build trees, and it can be distributed with your project so that it is easier for other users to cross compile it.
Running Executables Built in the Project¶
In some cases it is necessary that during a build, an executable is
invoked that was built earlier in the same build; this is usually the
case for code generators and similar tools. This does not work when
cross compiling, as the executables are built for the target platform
and cannot run on the build host (without the use of virtual machines,
compatibility layers, emulators, etc.). With CMake, these programs are
created using add_executable
, and executed with
add_custom_command
or add_custom_target
. The
following three options can be used to support these executables with
CMake. The old version of the CMake code could look something like this
add_executable(mygen gen.c)
get_target_property(mygenLocation mygen LOCATION)
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
COMMAND ${mygenLocation}
-o "${CMAKE_CURRENT_BINARY_DIR}/generated.h" )
Now we will show how this file can be modified so that it works when
cross-compiling. The basic idea is that the executable is built only
when doing a native build for the build host, and is then exported as
an executable target to a CMake script file. This file is then
included when cross-compiling, and the executable target for the
executable mygen will be loaded. An imported target with the same name
as the original target will be created. add_custom_command
recognizes target names as executables, so for the command in
add_custom_command
, simply the target name can be used; it is not
necessary to use the LOCATION property to obtain the path of the executable:
if(CMAKE_CROSSCOMPILING)
find_package(MyGen)
else()
add_executable(mygen gen.c)
export(TARGETS mygen FILE
"${CMAKE_BINARY_DIR}/MyGenConfig.cmake")
endif()
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
COMMAND mygen -o "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
)
With the CMakeLists.txt modified like this, the project can be cross-compiled. First, a native build has to be done in order to create the necessary mygen executable. After that, the cross-compiling build can begin. The build directory of the native build has to be given to the cross-compiling build as the location of the MyGen package, so that find_package(MyGen) can find it:
mkdir build-native; cd build-native
cmake ..
make
cd ..
mkdir build-cross; cd build-cross
cmake -DCMAKE_TOOLCHAIN_FILE=MyToolchain.cmake \
-DMyGen_DIR=~/src/build-native/ ..
make
Cross-Compiling Hello World¶
Now let’s actually start with the cross-compiling. The first step is to install a cross-compiling toolchain. If this is already installed, you can skip the next paragraph.
There are many different approaches and projects that deal with cross-compiling for Linux, ranging from free software projects working on Linux-based PDAs to commercial embedded Linux vendors. Most of these projects come with their own way to build and use the respective toolchain. Any of these toolchains can be used with CMake; the only requirement is that it works in the normal file system and does not expect a “sandboxed” environment, like for example the Scratchbox project.
An easy-to-use toolchain with a relatively complete target environment is the Embedded Linux Development Toolkit (http://www.denx.de/wiki/DULG/ELDK). It supports ARM, PowerPC, and MIPS as target platforms. ELDK can be downloaded from ftp://ftp.sunet.se/pub/Linux/distributions/eldk/. The easiest way is to download the ISOs, mount them, and then install them:
mkdir mount-iso/
sudo mount -tiso9660 mips-2007-01-21.iso mount-iso/ -o loop
cd mount-iso/
./install -d /home/alex/eldk-mips/
...
Preparing... ########################################### [100%]
1:appWeb-mips_4KCle ########################################### [100%]
Done
ls /opt/eldk-mips/
bin eldk_init etc mips_4KC mips_4KCle usr var version
ELDK (and other toolchains) can be installed anywhere, either in the home directory or system-wide if there are more users working with them. In this example, the toolchain will now be located in /home/alex/eldk-mips/usr/bin/ and the target environment is in /home/alex/eldk-mips/mips_4KC/.
Now that a cross-compiling toolchain is installed, CMake has to be set up to use it. As already described, this is done by creating a toolchain file for CMake. In this example, the toolchain file looks like this:
# the name of the target operating system
set(CMAKE_SYSTEM_NAME Linux)
# which C and C++ compiler to use
set(CMAKE_C_COMPILER /home/alex/eldk-mips/usr/bin/mips_4KC-gcc)
set(CMAKE_CXX_COMPILER
/home/alex/eldk-mips/usr/bin/mips_4KC-g++)
# location of the target environment
set(CMAKE_FIND_ROOT_PATH /home/alex/eldk-mips/mips_4KC
/home/alex/eldk-mips-extra-install )
# adjust the default behavior of the FIND_XXX() commands:
# search for headers and libraries in the target environment,
# search for programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
The toolchain files can be located anywhere, but it is a good idea to
put them in a central place so that they can be reused in multiple
projects. We will save this file as
~/Toolchains/Toolchain-eldk-mips4K.cmake. The variables mentioned
above are set here: CMAKE_SYSTEM_NAME
, the C/C++
compilers, and CMAKE_FIND_ROOT_PATH
to specify where
libraries and headers for the target environment are located. The find
modes are also set up so that libraries and headers are searched for
in the target environment only, whereas programs are searched for in the host environment only. Now we will cross-compile the hello world project from Chapter 2
project(Hello)
add_executable(Hello Hello.c)
Run CMake, this time telling it to use the toolchain file from above:
mkdir Hello-eldk-mips
cd Hello-eldk-mips
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake ..
make VERBOSE=1
This should give you an executable that can run on the target platform. Thanks to the VERBOSE=1 option, you should see that the cross-compiler is used. Now we will make the example a bit more sophisticated by adding system inspection and install rules. We will build and install a shared library named Tools, and then build the Hello application which links to the Tools library.
include(CheckIncludeFiles)
check_include_files(stdio.h HAVE_STDIO_H)
set(VERSION_MAJOR 2)
set(VERSION_MINOR 6)
set(VERSION_PATCH 0)
configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)
add_library(Tools SHARED tools.cxx)
set_target_properties(Tools PROPERTIES
VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
SOVERSION ${VERSION_MAJOR})
install(FILES tools.h DESTINATION include)
install(TARGETS Tools DESTINATION lib)
There is no difference in a normal CMakeLists.txt; no special
prerequisites are required for cross-compiling. The CMakeLists.txt
checks that the header stdio.h is available and sets the version
number for the Tools library. These are configured into config.h,
which is then used in tools.cxx. The version number is also used to
set the version number of the Tools library. The library and headers
are installed to ${CMAKE_INSTALL_PREFIX
}/lib and
${CMAKE_INSTALL_PREFIX
}/include respectively. Running CMake gives this
result:
mkdir build-eldk-mips
cd build-eldk-mips
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake \
-DCMAKE_INSTALL_PREFIX=~/eldk-mips-extra-install ..
-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /home/alex/eldk-mips/usr/bin/mips_4KC-gcc
-- Check for working C compiler:
/home/alex/eldk-mips/usr/bin/mips_4KC-gcc -- works
-- Check size of void*
-- Check size of void* - done
-- Check for working CXX compiler: /home/alex/eldk-mips/usr/bin/mips_4KC-g++
-- Check for working CXX compiler:
/home/alex/eldk-mips/usr/bin/mips_4KC-g++ -- works
-- Looking for include files HAVE_STDIO_H
-- Looking for include files HAVE_STDIO_H - found
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alex/src/tests/Tools/build-mips
make install
Scanning dependencies of target Tools
[100%] Building CXX object CMakeFiles/Tools.dir/tools.o
Linking CXX shared library libTools.so
[100%] Built target Tools
Install the project...
-- Install configuration: ""
-- Installing /home/alex/eldk-mips-extra-install/include/tools.h
-- Installing /home/alex/eldk-mips-extra-install/lib/libTools.so
As can be seen in the output above, CMake detected the correct compiler, found the stdio.h header for the target platform, and successfully generated the Makefiles. The make command was invoked, which then successfully built and installed the library in the specified installation directory. Now we can build an executable that uses the Tools library and does some system inspection
project(HelloTools)
find_package(ZLIB REQUIRED)
find_library(TOOLS_LIBRARY Tools)
find_path(TOOLS_INCLUDE_DIR tools.h)
if(NOT TOOLS_LIBRARY OR NOT TOOLS_INCLUDE_DIR)
message FATAL_ERROR "Tools library not found")
endif()
set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
set(CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE TRUE)
include_directories("${TOOLS_INCLUDE_DIR}"
"${ZLIB_INCLUDE_DIR}")
add_executable(HelloTools main.cpp)
target_link_libraries(HelloTools ${TOOLS_LIBRARY}
${ZLIB_LIBRARIES})
set_target_properties(HelloTools PROPERTIES
INSTALL_RPATH_USE_LINK_PATH TRUE)
install(TARGETS HelloTools DESTINATION bin)
Building works in the same way as with the library; the toolchain file has to be used and then it should just work:
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake \
-DCMAKE_INSTALL_PREFIX=~/eldk-mips-extra-install ..
-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /home/alex/denx-mips/usr/bin/mips_4KC-gcc
-- Check for working C compiler:
/home/alex/denx-mips/usr/bin/mips_4KC-gcc -- works
-- Check size of void*
-- Check size of void* - done
-- Check for working CXX compiler: /home/alex/denx-mips/usr/bin/mips_4KC-g++
-- Check for working CXX compiler:
/home/alex/denx-mips/usr/bin/mips_4KC-g++ -- works
-- Found ZLIB: /home/alex/denx-mips/mips_4KC/usr/lib/libz.so
-- Found Tools library: /home/alex/denx-mips-extra-install/lib/libTools.so
-- Configuring done
-- Generating done
-- Build files have been written to:
/home/alex/src/tests/HelloTools/build-eldk-mips
make
[100%] Building CXX object CMakeFiles/HelloTools.dir/main.o
Linking CXX executable HelloTools
[100%] Built target HelloTools
Obviously CMake found the correct zlib and also libTools.so, which had been installed in the previous step.
Cross-Compiling for a Microcontroller¶
CMake can be used for more than cross-compiling to targets with operating systems, it is also possible to use it in development for deeply-embedded devices with small microcontrollers and no operating system at all. As an example, we will use the Small Devices C Compiler (http://sdcc.sourceforge.net), which runs under Windows, Linux, and Mac OS X, and supports 8 and 16 Bit microcontrollers. For driving the build, we will use MS NMake under Windows. As before, the first step is to write a toolchain file so that CMake knows about the target platform. For sdcc, it should look something like this
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER "c:/Program Files/SDCC/bin/sdcc.exe")
The system name for targets that do not have an operating system,
“Generic,” should be used as the CMAKE_SYSTEM_NAME
. The CMake platform
file for “Generic” doesn’t set up any specific features. All that it
assumes is that the target platform does not support shared libraries,
and so all properties will depend on the compiler and
CMAKE_SYSTEM_PROCESSOR
. The toolchain file above does not set the
FIND-related variables. As long as none of the find commands is used
in the CMake commands, this is fine. In many projects for small
microcontrollers, this will be the case. The CMakeLists.txt should
look like the following
project(Blink C)
add_library(blink blink.c)
add_executable(hello main.c)
target_link_libraries(hello blink)
There are no major differences in other CMakeLists.txt files. One
important point is that the language “C” is enabled explicitly using
the project
command. If this is not done, CMake will also
try to enable support for C++, which will fail as sdcc only has support
for C. Running CMake and building the project should work as usual:
cmake -G"NMake Makefiles" \
-DCMAKE_TOOLCHAIN_FILE=c:/Toolchains/Toolchain-sdcc.cmake ..
-- The C compiler identification is SDCC
-- Check for working C compiler: c:/program files/sdcc/bin/sdcc.exe
-- Check for working C compiler: c:/program files/sdcc/bin/sdcc.exe -- works
-- Check size of void*
-- Check size of void* - done
-- Configuring done
-- Generating done
-- Build files have been written to: C:/src/tests/blink/build
nmake
Microsoft (R) Program Maintenance Utility Version 7.10.3077
Copyright (C) Microsoft Corporation. All rights reserved.
Scanning dependencies of target blink
[ 50%] Building C object CMakeFiles/blink.dir/blink.rel
Linking C static library blink.lib
[ 50%] Built target blink
Scanning dependencies of target hello
[100%] Building C object CMakeFiles/hello.dir/main.rel
Linking C executable hello.ihx
[100%] Built target hello
This was a simple example using NMake with sdcc with the default
settings of sdcc. Of course, more sophisticated project layouts are
possible. For this kind of project, it is also a good idea to set up
an install directory where reusable libraries can be installed, so it
is easier to use them in multiple projects. It is normally necessary
to choose the correct target platform for sdcc; not everybody uses
i8051, which is the default for sdcc. The recommended way to do this
is via setting CMAKE_SYSTEM_PROCESSOR
.
This will cause CMake to search for and load the platform file Platform/Generic-SDCC-C-${CMAKE_SYSTEM_PROCESSOR}.cmake. As this happens, right before loading Platform/Generic-SDCC-C.cmake, it can be used to set up the compiler and linker flags for the specific target hardware and project. Therefore, a slightly more complex toolchain file is required
get_filename_component(_ownDir
"${CMAKE_CURRENT_LIST_FILE}" PATH)
set(CMAKE_MODULE_PATH "${_ownDir}/Modules" ${CMAKE_MODULE_PATH})
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER "c:/Program Files/SDCC/bin/sdcc.exe")
set(CMAKE_SYSTEM_PROCESSOR "Test_DS80C400_Rev_1")
# here is the target environment located
set(CMAKE_FIND_ROOT_PATH "c:/Program Files/SDCC"
"c:/ds80c400-install" )
# adjust the default behavior of the FIND_XXX() commands:
# search for headers and libraries in the target environment
# search for programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
This toolchain file contains a few new settings; it is also about the
most complicated toolchain file you should ever need.
CMAKE_SYSTEM_PROCESSOR
is set to Test_DS80C400_Rev_1, an
identifier for the specific target hardware. This has the effect that
CMake will try to load
Platform/Generic-SDCC-C-Test_DS80C400_Rev_1.cmake. As this file does
not exist in the CMake system module directory, the CMake variable
CMAKE_MODULE_PATH
has to be adjusted so that this file can
be found. If this toolchain file is saved to
c:/Toolchains/sdcc-ds400.cmake, the hardware-specific file should be
saved in c:/Toolchains/Modules/Platform/. An example of this is shown
below:
set(CMAKE_C_FLAGS_INIT "-mds390 --use-accelerator")
set(CMAKE_EXE_LINKER_FLAGS_INIT "")
This will select the DS80C390 as the target platform and add the –use-accelerator argument to the default compile flags. In this example the “NMake Makefiles” generator was used. In the same way e.g. the “MinGW Makefiles” generator could be used for a GNU make from MinGW, or another Windows version of GNU make, are available. At least version 3.78 is required, or the “Unix Makefiles” generator under UNIX. Also, any Makefile-based, IDE-project generators could be used; e.g. the Eclipse, CodeBlocks, or the KDevelop3 generator.
Cross-Compiling a Complex Project - VTK¶
Building a complex project is a multi-step process. Complex in this
case means that the project uses tests that run executables, and that
it builds executables which are used later in the build to generate
code (or something similar). One such project is VTK, which uses
several try_run
tests and creates several code generators.
When running CMake on the project, every try_run
command
will produce an error message; at the end there will be a
TryRunResults.cmake
file in the build directory. You need to go
through all of the entries of this file and fill in the appropriate
values. If you are uncertain about the correct result, you can also
try to execute the test binaries on the real target platform, where
they are saved in the binary directory.
VTK contains several code generators, one of which is
ProcessShader. These code generators are added using
add_executable
; get_target_property(LOCATION)
is used
to get the locations of the resulting binaries, which are then used
in add_custom_command
or add_custom_target
commands. Since the cross-compiled executables cannot be executed
during the build, the add_executable
calls are
surrounded by if (NOT CMAKE_CROSSCOMPILING)
commands and the
executable targets are imported into the project using the
add_executable
command with the IMPORTED
option. These
import statements are in the file VTKCompileToolsConfig.cmake
,
which does not have to be created manually, but it is created by a
native build of VTK.
In order to cross-compile VTK, you need to:
Install a toolchain and create a toolchain file for CMake.
Build VTK natively for the build host.
Run CMake for the target platform.
Complete
TryRunResults.cmake
.Use the
VTKCompileToolsConfig.cmake
file from the native build.Build.
So first, build a native VTK for the build host using the standard procedure.
cd VTK
mkdir build-native; cd build-native
ccmake ..
make
Ensure that all required options are enabled using ccmake; e.g. if you
need Python wrapping for the target platform, you must enable Python
wrapping in build-native/
. Once this build has finished, there
will be a VTKCompileToolsConfig.cmake
file in
build-native/
. If this succeeded, we can continue to cross
compiling the project, in this example for an IBM BlueGene
supercomputer.
cd VTK
mkdir build-bgl-gcc
cd build-bgl-gcc
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-BlueGeneL-gcc.cmake \
-DVTKCompileTools_DIR=~/VTK/build-native/ ..
This will finish with an error message for each try_run
and a
TryRunResults.cmake file
, that you have to complete as described
above. You should save the file to a safe location, or it will be
overwritten on the next CMake run.
cp TryRunResults.cmake ../TryRunResults-VTK-BlueGeneL-gcc.cmake
ccmake -C ../TryRunResults-VTK-BlueGeneL-gcc.cmake .
...
make
On the second run of ccmake, all the other arguments can be skipped as they are now in the cache. It is possible to point CMake to the build directory that contains a CMakeCache.txt, so CMake will figure out that this is the build directory.
Some Tips and Tricks¶
Dealing with try_run tests¶
In order to make cross compiling your project easier, try to avoid
try_run
tests and use other methods to test something instead. If
you cannot avoid try_run
tests, try to use only the
exit code from the run and not the output of the process. That way it
will not be necessary to set both the exit code and the stdout and
stderr variables for the try_run
test when
cross-compiling. This allows the OUTPUT_VARIABLE
or the
RUN_OUTPUT_VARIABLE
options for try_run
to be omitted.
If you have done that, created and completed a correct
TryRunResults.cmake
file for the target platform, you might
consider adding this file to the sources of the project, so that it
can be reused by others. These files are per-target, per-toolchain.
Target platform and toolchain issues¶
If your toolchain is unable to build a simple program without special arguments, like e.g. a linker script file or a memory layout file, the tests CMake does initially will fail. To make it work the CMake module CMakeForceCompiler offers the following macros:
CMAKE_FORCE_SYSTEM(name version processor),
CMAKE_FORCE_C_COMPILER(compiler compiler_id sizeof_void_p)
CMAKE_FORCE_CXX_COMPILER(compiler compiler_id).
These macros can be used in a toolchain file so that the required variables will be preset and the CMake tests avoided.
RPATH handling under UNIX¶
For native builds, CMake builds executables and libraries by default with RPATH. In the build tree, the RPATH is set so that the executables can be run from the build tree; i.e. the RPATH points into the build tree. When installing the project, CMake links the executables again, this time with the RPATH for the install tree, which is empty by default.
When cross-compiling you probably want to set up RPATH handling differently. As the executable cannot run on the build host, it makes more sense to build it with the install RPATH right from the start. There are several CMake variables and target properties for adjusting RPATH handling.
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "<whatever you need>")
With these two settings, the targets will be built with the install
RPATH instead of the build RPATH, which avoids the need to link them
again when installing. If you don’t need RPATH support in your
project, you don’t need to set CMAKE_INSTALL_RPATH
; it is empty by
default.
Setting CMAKE_INSTALL_RPATH_USE_LINK_PATH
to TRUE
is useful
for native builds, since it automatically collects the RPATH from all
libraries against which a targets links. For cross-compiling it should
be left at the default setting of FALSE
, because on the target the
automatically generated RPATH will be wrong in most cases; it will
probably have a different file system layout than the build host.