Using CMake as a Build Tool for Large Projects

Clickthe blue text

Follow us

This article mainly describes the basic usage of CMake. In previous documents, I covered two build tools, Makefile and Autotools. Related articles are as follows:

  • “What is Makefile on Linux?”

  • “What knowledge should you know before working with Makefile?”

  • “Practical examples of Makefile”

  • “Compiling with Autotools and Yocto”

Previously, I described these two tools, where the ultimate goal of Autotools is to implement Makefile. In the last article, when we introduced the Autotools tool, we mentioned that it is designed to solve the issue of the complex syntax structure of Makefile, making it more convenient. However, there are still many experts who feel that Autotools has some issues. Thus, a group of experts, who always have nothing to do, began to think about how to optimize these problems. Hence, another tool was born—CMake. The ultimate goal of CMake is also to generate Makefile. Therefore, it is recommended to learn about these two tools after understanding the content of Makefile. However, it must be stated that just because CMake has emerged, it does not mean you should stop learning Makefile and Autotools. There is no best among the three; it depends on the application scenario before making a choice. For example, for projects with only a few files, using Makefile is the best choice, while Autotools and CMake are mostly used in large projects. Next, let’s talk about the new tool:Using CMake as a Build Tool for Large ProjectsFeatures of CMakeUsing CMake as a Build Tool for Large Projects

  • Open source.

  • Cross-platform, capable of generating native compilation configuration files: on Linux, it generates Makefile; on macOS, it generates Xcode; and on Windows, it generates MSVC project files.

  • Can manage large projects.

  • Simplifies the compilation build process, and the toolchain is very simple: cmake + make.

  • High efficiency, faster than Autotools. The main reason is that CMake does not include libtool in its toolchain.

  • Extensible; specific functional modules can be written for CMake to enhance its capabilities.

Using CMake as a Build Tool for Large ProjectsHow to Use CMake to Compile a ProjectUsing CMake as a Build Tool for Large Projects

A simple cmake example requires only two files: CMakeLists.txt and main.c. Let’s prepare these two files. Among them, main.c is the source file we want to compile, and CMakeLists.txt is crucial as it tells cmake how to compile, which can be understood as the compilation rules. Now, let’s have our first experience:

main.c:

#include "stdio.h"
int main(int argc, char *argv[]){        printf("Rice CMake!!!\n");
        return 0;
}

CMakeLists.txt:

PROJECT(RICE)
SET(SRC_LIST main.c)
MESSAGE(STATUS "THIS IS BINARY DIR " ${PROJECT_BINARY_DIR})
MESSAGE(STATUS "THIS IS SOURCE DIR " ${PROJECT_SOURCE_DIR})
ADD_EXECUTABLE(rice ${SRC_LIST})

Note: Pay attention to the naming convention of CMakeLists.txt, especially the case sensitivity; otherwise, compilation will fail.

Compilation Test: First, execute the command cmake .. This command will build the Makefile based on the rules in CMakeLists.txt, then execute the make command to generate the executable program, and finally run ./rice to check the result:

rice@rice:~/rice_file/cmake$ cmake .
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- THIS IS BINARY DIR /home/rice/rice_file/cmake
-- THIS IS SOURCE DIR /home/rice/rice_file/cmake
-- Configuring done
-- Generating done
-- Build files have been written to: /home/rice/rice_file/cmake
rice@rice:~/rice_file/cmake$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt  main.c  Makefile
rice@rice:~/rice_file/cmake$ make
Scanning dependencies of target rice
[ 50%] Building C object CMakeFiles/rice.dir/main.c.o
[100%] Linking C executable rice
[100%] Built target rice
rice@rice:~/rice_file/cmake$ ls
CMakeCache.txt  CMakeFiles  cmake_install.cmake  CMakeLists.txt  main.c  Makefile  rice
rice@rice:~/rice_file/cmake$ ./rice
Rice CMake!!!
rice@rice:~/rice_file/cmake$ 

Using CMake as a Build Tool for Large ProjectsCMake Syntax ExplanationUsing CMake as a Build Tool for Large ProjectsLet’s describe the above CMakeLists.txt:

Command PROJECT
Syntax PROJECT(projectname [CXX] [C] [Java])
Description Used to specify the project name and can specify the supported languages (the list of supported languages can be ignored, as all languages are supported by default). This command implicitly defines two cmake variables: <projectname>_BINARY_DIR and <projectname>_SOURCE_DIR. cmake helps us predefine PROJECT_BINARY_DIR and PROJECT_SOURCE_DIR variables. It is recommended to use these two variables, as modifying the project name will not affect them. If you use <projectname>_SOURCE_DIR, after modifying the project name, you will need to modify these variables as well.
Command SET
Syntax SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
Description The SET command can be used to explicitly define variables, for example, SET(SRC_LIST main.c). If there are multiple source files, it can also be defined as SET(SRC_LIST main.c t1.c t2.c).
Command MESSAGE
Syntax MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] “message to display” …)
Description This command is used to output user-defined information to the terminal, which includes three types: SEND_ERROR: generates an error and skips the generation process; STATUS: outputs information prefixed with “-“; FATAL_ERROR: immediately terminates all cmake processes.
Command ADD_EXECUTABLE
Syntax ADD_EXECUTABLE([BINARY] [SOURCE_LIST])
Description This defines that the project will generate an executable file named [BINARY], with the related source files being the ones defined in SOURCE_LIST.

Using CMake as a Build Tool for Large ProjectsInternal and External BuildsUsing CMake as a Build Tool for Large Projects

In the above example, we used an internal build, which will generate more temporary files than the source files we’ve written, and they will be in the same directory. When we want to delete these intermediate files, it can be particularly troublesome. So is there a way to make it cleaner and simpler? The answer is to use an external build.

An external build can be simply understood as separating the intermediate files generated by cmake from the source files, ensuring they are not in the same directory. As follows:

rice@rice:~/rice_file/cmake$ mkdir build
rice@rice:~/rice_file/cmake$ cd build/
rice@rice:~/rice_file/cmake/build$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- THIS IS BINARY DIR /home/rice/rice_file/cmake/build
-- THIS IS SOURCE DIR /home/rice/rice_file/cmake/
-- Configuring done
-- Generating done
-- Build files have been written to: /home/rice/rice_file/cmake/build
rice@rice:~/rice_file/cmake/build$ cd ..
rice@rice:~/rice_file/cmake$ tree -L 2
.├── build│   ├── CMakeCache.txt│   ├── CMakeFiles│   ├── cmake_install.cmake│   └── Makefile├── CMakeLists.txt└── main.c
2 directories, 5 files
rice@rice:~/rice_file/cmake$

Process: Create a directory build, then execute cmake in the build directory to generate the intermediate files there, keeping the source files clean. Therefore, the following explanations will adopt the external build method.

Using CMake as a Build Tool for Large ProjectsA More Perfect ProjectUsing CMake as a Build Tool for Large Projects

For a slightly more complete project, how should we proceed? The steps are as follows:

  • Create a subdirectory src for placing the project source code main.c and CMakeLists.txt files.

The content of CMakeLists.txt in the src directory is as follows:

SET(SRC_LIST main.c)
ADD_EXECUTABLE(rice ${SRC_LIST})
INSTALL(TARGETS rice RUNTIME DESTINATION bin)
  • Add a subdirectory doc to place project documentation rice.txt. (You can write any content in rice.txt for standardization purposes.)

  • In the project directory, add text files COPYRIGHT and README. (Similarly, write any content in these for standardization purposes.)

  • Place the built executable file (rice) into the bin directory of the build directory.

  • Finally, install these files: Install the executable file (rice) to /tmp/usr/bin and the contents of the doc directory, COPYRIGHT, and README to /tmp/usr/share/doc/cmake.

    The content of CMakeLists.txt in the project directory is as follows:

PROJECT(RICE)
ADD_SUBDIRECTORY(src bin)
INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/rice)
INSTALL(PROGRAMS runrice.sh DESTINATION bin)
INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/rice)

Compilation result directory:

rice@rice:~/rice_file/cmake$ tree -L 3
.├── build│   ├── bin│   │   ├── CMakeFiles│   │   ├── cmake_install.cmake│   │   ├── Makefile│   │   └── rice│   ├── CMakeCache.txt│   ├── CMakeFiles│   │   ├── 3.10.2│   │   ├── cmake.check_cache│   │   ├── CMakeDirectoryInformation.cmake│   │   ├── CMakeOutput.log│   │   ├── CMakeTmp│   │   ├── feature_tests.bin│   │   ├── feature_tests.c│   │   ├── feature_tests.cxx│   │   ├── Makefile2│   │   ├── Makefile.cmake│   │   ├── progress.marks│   │   └── TargetDirectories.txt│   ├── cmake_install.cmake│   ├── install_manifest.txt│   └── Makefile├── CMakeLists.txt├── COPYRIGHT├── doc│   └── rice.txt├── README└── src    ├── CMakeLists.txt    └── main.c
8 directories, 24 files
rice@rice:~/rice_file/cmake$

Among them:

Command ADD_SUBDIRECTORY
Syntax ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
Description This command is used to add a subdirectory that contains source files to the current project, and you can specify where to store the intermediate and target binaries. The EXCLUDE_FROM_ALL parameter means to exclude this directory from the build process; for example, the project’s example may need to be built separately after the project is complete (of course, you can also solve this type of problem by defining dependencies).

The INSTALL command is used to define installation rules, and the content can include target binaries, dynamic libraries, static libraries, files, directories, scripts, etc. The INSTALL command includes various installation types, which we need to explain one by one:

Type Target Files
Command INSTALL
Syntax

INSTALL(TARGETS targets…

[[ARCHIVE|LIBRARY|RUNTIME]

[DESTINATION <dir>]

[PERMISSIONS permissions…]

[CONFIGURATIONS

[Debug|Release|…]]

[COMPONENT <component>]

[OPTIONAL]

] […])

Description

The targets following the TARGETS parameter are the target files defined by ADD_EXECUTABLE or ADD_LIBRARY, which can be executable binaries, dynamic libraries, or static libraries. There are three corresponding target types: ARCHIVE refers to static libraries, LIBRARY refers to dynamic libraries, and RUNTIME refers to executable binaries. The DESTINATION defines the installation path.

Type Regular Files
Command INSTALL
Syntax

INSTALL(FILES files… DESTINATION <dir>

[PERMISSIONS permissions…]

[CONFIGURATIONS [Debug|Release|…]]

[COMPONENT <component>]

[RENAME <name>] [OPTIONAL])

Description

This can be used to install general files and can specify access permissions; the filename is the relative path from where this command is located. If no permissions are defined, the installed permissions will be: OWNER_WRITE, OWNER_READ, GROUP_READ, and WORLD_READ, which is equivalent to permission 644.

Type Non-target Executable Programs (e.g., scripts)
Command INSTALL
Syntax

INSTALL(PROGRAMS files… DESTINATION <dir>

[PERMISSIONS permissions…]

[CONFIGURATIONS [Debug|Release|…]]

[COMPONENT <component>]

[RENAME <name>] [OPTIONAL])

Description

Same usage as the FILES command, but the only difference is that the installed permissions will be: OWNER_EXECUTE, GROUP_EXECUTE, and WORLD_EXECUTE, which is equivalent to permission 755.

Type Directories
Command INSTALL
Syntax

INSTALL(DIRECTORY dirs… DESTINATION <dir>

[FILE_PERMISSIONS permissions…]

[DIRECTORY_PERMISSIONS permissions…]

[USE_SOURCE_PERMISSIONS]

[CONFIGURATIONS [Debug|Release|…]]

[COMPONENT <component>]

[[PATTERN <pattern> | REGEX <regex>]

[EXCLUDE] [PERMISSIONS permissions…]] […])

Description

Mainly introduces the DIRECTORY, PATTERN, and PERMISSIONS parameters:

DIRECTORY: the relative path from the source directory.

PATTERN: used for filtering with regular expressions.

PERMISSIONS: used to specify permissions for files filtered by PATTERN.

The above project is compiled:

rice@rice:~/rice_file/cmake$ cmake -DCMAKE_INSTALL_PREFIX=/tmp/usr ..
rice@rice:~/rice_file/cmake$ make
rice@rice:~/rice_file/cmake$ make install

After execution, all results can be viewed in the /tmp/usr directory, as shown below:

rice@rice:/tmp/usr$ tree .
├── bin│   └── rice
└── share    └── doc        └── cmake            └── rice                ├── COPYRIGHT                ├── README                └── rice.txt
5 directories, 4 files
rice@rice:/tmp/usr$ 

If there are any inaccuracies, feel free to discuss with the author…

Using CMake as a Build Tool for Large ProjectsUsing CMake as a Build Tool for Large ProjectsUsing CMake as a Build Tool for Large Projects

I will continue to update the article and learning materials.

You can add the author’s WeChat to discuss and learn together.

Using CMake as a Build Tool for Large Projects

Author’s WeChat ID: wueroo1314

To get the group chat, please add the author’s WeChat

Rice DIY

WeChat ID: Rice_DIY

Technology | Open Source | Sharing

Long press to follow…

Using CMake as a Build Tool for Large Projects

Leave a Comment