CMake Module Detailed Explanation: Mastering Usage and Custom Modules

CMake Module Detailed Explanation: Mastering Usage and Custom Modules

1. Introduction

This article will focus on the usage of system-defined Find modules and how to write your own Find modules. The system provides various other modules, which generally need to be explicitly called using the INCLUDE directive. The FIND_PACKAGE directive is a special case that can directly invoke predefined modules.

In fact, managing a project solely relying on the basic commands provided by CMake is a very complex task. Therefore, CMake is designed to be an extensible architecture that allows the extension of CMake by writing some generic modules.

This article will first introduce the usage of the FindCURL module provided by CMake. Then, we will write a FindHello.cmake module based on shared libraries.

2. Using Find Modules

FIND_PACKAGE directive:

FIND_PACKAGE(<name> [major.minor] [QUIET] [NO_MODULE]
[[REQUIRED|COMPONENTS] [components...]])

Various parameters can be used, such as QUIET and REQUIRED. The meaning of these parameters indicates whether this shared library is essential for the project. If this parameter is used, it indicates that this linking library is a must-have library. If this library cannot be found, the project cannot be compiled.

2.1. Preparation

(1) Create a t5 directory to store our CURL examples.

mkdir t5

(2) Create a src directory and a src/main.c file.

cd t5
mkdir src
cd src
vim main.c

The content is as follows:

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
FILE *fp = NULL;
int write_data(void *ptr, size_t size, size_t nmemb, void *stream) {
  int written = fwrite(ptr, size, nmemb, (FILE *)fp);
  return written;
}
int main(void) {
  const char * path = "/tmp/curl-test";
  const char * mode = "w";
  fp = fopen(path, mode);
  curl_global_init(CURL_GLOBAL_ALL);
  CURL *curl = curl_easy_init();
  curl_easy_setopt(curl, CURLOPT_URL, "http://www.baidu.com");
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
  CURLcode res = curl_easy_perform(curl);
  curl_easy_cleanup(curl);
  return 0;
}

This code retrieves the homepage from http://www.linux-ren.org using curl and writes it to the /tmp/curl-test file.

(3) Create the main project file CMakeLists.txt.

cd t5
vim CMakeLists.txt

The content is as follows:

PROJECT(CURLTEST)
ADD_SUBDIRECTORY(src)

(4) Create src/CMakeLists.txt with the following content:

ADD_EXECUTABLE(curltest main.c)

Now it is naturally not possible to compile. We need to add the header file path and library file for curl.

2.2. Adding Header File Path and Library File

(1) Method 1:

Directly add using INCLUDE_DIRECTORIES and TARGET_LINK_LIBRARIES directives. We can add directly in src/CMakeLists.txt:

INCLUDE_DIRECTORIES(/usr/include) 
TARGET_LINK_LIBRARIES(curltest curl)

Then create a build directory for external building.

(2) Method 2: Use the FindCURL module.

Now use the FindCURL module provided by CMake, and add the following to src/CMakeLists.txt:

FIND_PACKAGE(CURL)
IF(CURL_FOUND)
INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR})
TARGET_LINK_LIBRARIES(curltest ${CURL_LIBRARY})
ELSE(CURL_FOUND)
MESSAGE(FATAL_ERROR "CURL library not found")
ENDIF(CURL_FOUND)

For system-defined Find<name>.cmake modules, the usage method is generally as shown above. Each module will define the following variables:

<name>_FOUND
<name>_INCLUDE_DIR or <name>_INCLUDES
<name>_LIBRARY or <name>_LIBRARIES

You can use <name>_FOUND to determine whether the module was found. If not found, some features may be disabled, a reminder may be given, or compilation may be terminated as needed. The above example reports a fatal error and halts the build.

If <name>_FOUND is true, then <name>_INCLUDE_DIR is added to INCLUDE_DIRECTORIES and <name>_LIBRARY is added to TARGET_LINK_LIBRARIES.

Then create a build directory for external building:

mkdir build
cd build
cmake ..

If the library does not exist, an error will be reported:

-- The C compiler identification is GNU 8.4.0
-- The CXX compiler identification is GNU 8.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Could NOT find CURL (missing: CURL_LIBRARY CURL_INCLUDE_DIR) 
CMake Error at src/CMakeLists.txt:7 (MESSAGE):
  CURL library not found


CMake Warning (dev) in CMakeLists.txt:
  No cmake_minimum_required command is present.  A line of code such as

    cmake_minimum_required(VERSION 3.21)

  should be added at the top of the file.  The version specified may be lower
  if you wish to support older CMake versions for this project.  For more
  information run "cmake --help-policy CMP0000".
This warning is for project developers.  Use -Wno-dev to suppress it.

-- Configuring incomplete, errors occurred!
See also "/home/fly/workspace/cmakeProj/t5/build/CMakeFiles/CMakeOutput.log".

2.3. Using <name>_FOUND to Control Project Features

Let’s look at a more complex example where we use <name>_FOUND to control project features:

SET(mySources viewer.c)
SET(optionalSources)
SET(optionalLibs)
FIND_PACKAGE(JPEG)
 
IF(JPEG_FOUND)
SET(optionalSources ${optionalSources} jpegview.c)
INCLUDE_DIRECTORIES( ${JPEG_INCLUDE_DIR} )
SET(optionalLibs ${optionalLibs} ${JPEG_LIBRARIES} )
ADD_DEFINITIONS(-DENABLE_JPEG_SUPPORT)
ENDIF(JPEG_FOUND)
 
IF(PNG_FOUND)
SET(optionalSources ${optionalSources} pngview.c)
INCLUDE_DIRECTORIES( ${PNG_INCLUDE_DIR} )
SET(optionalLibs ${optionalLibs} ${PNG_LIBRARIES} )
ADD_DEFINITIONS(-DENABLE_PNG_SUPPORT)
ENDIF(PNG_FOUND)
 
ADD_EXECUTABLE(viewer ${mySources} ${optionalSources} ) TARGET_LINK_LIBRARIES(viewer ${optionalLibs}

This controls whether the program supports JPEG functionality based on whether the system provides the JPEG library.

3. Writing Custom Find Modules

3.1. Preparation

Write your own FindHello module.

(1) Create a t6 directory in cmake/ and create a cmake directory to store our custom FindHELLO.cmake module. Also create a src directory to store our source files.

mkdir t6
cd t6
mkdir cmake
mkdir src

(2) Define the cmake/FindHELLO.cmake module.

FIND_PATH(HELLO_INCLUDE_DIR hello.h /usr/include/hello /usr/local/include/hello)
FIND_LIBRARY(HELLO_LIBRARY NAMES hello PATH /usr/lib /usr/local/lib)

IF (HELLO_INCLUDE_DIR AND HELLO_LIBRARY)
    SET(HELLO_FOUND TRUE)
ENDIF (HELLO_INCLUDE_DIR AND HELLO_LIBRARY)

IF (HELLO_FOUND) 
    IF (NOT HELLO_FIND_QUIETLY)
        MESSAGE(STATUS "Found Hello: ${HELLO_LIBRARY}")
    ENDIF (NOT HELLO_FIND_QUIETLY)
ELSE (HELLO_FOUND)
    IF (HELLO_FIND_REQUIRED)
        MESSAGE(FATAL_ERROR "Could not find hello library")
    ENDIF (HELLO_FIND_REQUIRED)
ENDIF (HELLO_FOUND)

Various parameters can be used, such as QUIET, which corresponds to the HELLO_FIND_QUIETLY in our FindHELLO module. If this parameter is not specified, it will execute:

MESSAGE(STATUS "Found Hello: ${HELLO_LIBRARY}")

The REQUIRED parameter indicates whether this shared library is essential for the project. If this parameter is used, it indicates that this linking library is a must-have library. This corresponds to the HELLO_FIND_REQUIRED variable in the FindHELLO.cmake module.

In the above module, we define HELLO_FOUND, HELLO_INCLUDE_DIR, and HELLO_LIBRARY variables for developers to use in the FIND_PACKAGE directive.

3.2. CMake Module

The hello library uses the example introduced in the previous article.

Next, create src/main.c with the following content:

#include <hello.h>
int main()
{
    HelloFunc();
    return 0;
} 

Create the src/CMakeLists.txt file with the following content:

FIND_PACKAGE(HELLO)
IF(HELLO_FOUND)
ADD_EXECUTABLE(hello main.c)
INCLUDE_DIRECTORIES(${HELLO_INCLUDE_DIR})
TARGET_LINK_LIBRARIES(hello ${HELLO_LIBRARY})
ENDIF(HELLO_FOUND)

To ensure that the project can find the FindHELLO.cmake module (stored in the cmake directory within the project), we add the following to the main project file CMakeLists.txt:

SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

3.3. Using the Custom FindHELLO Module to Build the Project

Still using the external build method, create a build directory and run:

mkdir build
cd build
cmake ..

We can see from the output:

Found Hello: /usr/lib/libhello.so

If we change the above to FIND_PACKAGE(HELLO QUIET), we will not see the above output.

Next, we can use the make command to build the project and run:

./src/hello 

We can get the output:

Hello World

This indicates that the project was successfully built.

3.4. If the hello library is not found

We can try moving /usr/lib/libhello.x to the /tmp directory. Thus, according to the definition of the FindHELLO module, the hello library will not be found. Let’s look at the build results:

cmake ..

It can still build successfully, but at this point, it cannot be compiled.

Change FIND_PACKAGE(HELLO) to FIND_PACKAGE(HELLO REQUIRED) to define the hello library as an essential shared library for the project.

Now run cmake … again:

We get the following output:

CMake Error: Could not find hello library.

Because libhello.x cannot be found, the entire Makefile generation process is aborted due to an error.

4. Conclusion

Using the system-provided Find<NAME> modules and learning how to write Find<NAME> modules and how to use these modules in projects.

Welcome to follow my public account

Daily article push

CMake Module Detailed Explanation: Mastering Usage and Custom Modules

Long press the image above to recognize the QR code to follow

Leave a Comment