Skip to content

Utilizing CMake to Build and Test

Chun Cai edited this page Aug 4, 2021 · 3 revisions

For most projects, there is a build process generating executable from source files. Build systems are all very similar, including dependencies, targets, and rules from one to another. You tell the build sytem that you want to build a particular target, and its job is to find all the transitive dependencies of the target, and then apply the rules to produce intermediate targets all the way until the final target has been produced.

To fully automate building procedure, we recommend using CMake. CMake is able to find dependencies and build targets in a friendly grammar, as well as good integration to testing pipelines.

Principle

There is one CMakeLists.txt in each directory. It controls the behavior of this directory, e.g. what source files does this directory contain, how to build these source files, what subdirectory should it include, etc.

For the CMakeLists.txt file under root directory, it configures the whole project as an entrypoint to cmake, defining basis of the building process.

Our goal is to write a universal CMakeLists for building in different platform, with optional features, and under varies of dependencies. This can be achieved by either cmake detects automatically (e.g. find_package() in cmake), or defined manually (e.g. cmake --PW_ONLY to build ABACUS with plane wave method only)

For a Project

See abacus-develop/CMakeLists.txt.

There are three (eternal) formulae: dependency, target, and rules.

Dependency

When we talk about "dependency" in building procedure, what really matters are: include directories, definitions, and libraries. To introduce a dependency package into the program, we need to configure those three things.

Luckily, CMake community provides a convenient approach for commonly-used packages (e.g. Boost, FFTW, CUDA). It's quite simple to do so:

# CMakeLists.txt
find_package(Boost)
if(Boost_FOUND)
  include_directories(${Boost_INCLUDE_DIRS})
  add_definitions(-DUSE_BOOST)
  target_link_libraries(${ABACUS_BIN_NAME} Boost::<component>)
endif()

The find_package function will get everything done, say, defining result variables, including Boost_FOUND, Boost_INCLUDE_DIRS, Boost_LIBRARIES. After that, just add Boost_INCLUDE_DIRS variable to the list of include directories.

Generally, a macro flag is defined to use the control sequence in source files:

#ifdef USE_BOOST
  //use boost to do something
  #include <boost/array.hpp>
  boost::array<int, 4> cc = { 1,1,1,2 };
#else
  //use other method
  #include <array>
  std::array<int, 4> cc = { 1,1,1,2 };
#endif

For non-header-only dependencies, a library path is offered. Use target_link_libraries() to link your executable with libraries.

Note: CMake will provide full path to a library for linker, so you don't need to set LIBRARY_PATH environment variable by yourself. But, don't forget to ensure path to dynamic libs is in LD_LIBRARY_PATH.

However, for some less common dependencies, you have to do things above by writing a Find.cmake under modules/. We'll discuss this advanced usage later.

Target

  1. In CMakeLists.txt under each subdirectory: add_library(<your-module-name> OBJECT <your-source-files>). Each subdirectory should be a relatively isolated part of the project, just like modules.
  2. add_subdirectory(<subdirs>) in root dir. (Of course you can do this on subdirectory, if there are any configured sub-subdirectories.) This will instruct cmake to look for <subdir>/CMakeLists.txt.
  3. add_executable(<executable-name> <path-to-entrypoint>). Generally, the entrypoint is the source code containing main() function.
  4. target_link_libraries(<module-names>). After step 1, each subdirectory generates a library. Similar to dependencies, link them to the executable file.

Rule

Some common usage:

  • set(CMAKE_CXX_STANDARD 14)
  • add_compile_options(-O2 -g)
  • install(<path-to-install-dir>)

How do I add a new feature?

Remember the basic control sequence:

  • add an option to CMakeLists.txt in project root directory;
  • if the flag is defined, include new library/source code;
  • test with cmake . -D<your-flag>.