

Introduction:
In the previous article, we learned about one method of custom detection of external libraries in CMake. This article will demonstrate how to locate the ZeroMQ library on the system by writing a find module, enabling detection of this library on non-operating systems.

✦
Project Structure
✦
.
├── CMakeLists.txt
├── FindZeroMQ.cmake
├── zmq_client.cpp
└── zmq_server.cpp
Project Address:
https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter3/06
Note: The related cpp source code is the same as in the previous article.
✦
CMakeLists.txt
✦
CMakeLists.txt
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project(test_zmq LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_PREFIX_PATH /opt/zmq)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
find_package(ZeroMQ REQUIRED)
add_executable(hw_server zmq_server.cpp)
target_include_directories(hw_server
PRIVATE ${ZeroMQ_INCLUDE_DIRS}
)
target_link_libraries(hw_server
PRIVATE ${ZeroMQ_LIBRARIES}
)
add_executable(hw_client zmq_client.cpp)
target_include_directories(hw_client
PRIVATE ${ZeroMQ_INCLUDE_DIRS}
)
target_link_libraries(hw_client
PRIVATE ${ZeroMQ_LIBRARIES}
)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
This line of code is used to add a directory path to the CMAKE_MODULE_PATH variable in CMake. Typically, CMAKE_MODULE_PATH is used to store custom CMake module files, which can be utilized in the project’s CMakeLists.txt file through commands like include.
Specifically, this line of code adds ${CMAKE_CURRENT_SOURCE_DIR} to CMAKE_MODULE_PATH. ${CMAKE_CURRENT_SOURCE_DIR} refers to the directory where the current CMakeLists.txt is located, i.e., the source code directory. The purpose of this is to inform CMake to look for custom CMake modules in this directory.
Thus, CMake can find our custom FindZeroMQ.cmake module.
The main CMakeLists.txt of this example differs from the one used in the previous article when using FindZeroMQ.cmake. This module uses find_path and find_library built-in CMake commands to search for ZeroMQ header files and libraries, and uses find_package_handle_standard_args to set the relevant variables.
FindZeroMQ.cmake
if(NOT ZeroMQ_ROOT)
set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}")
endif()
if(NOT ZeroMQ_ROOT)
find_path(_ZeroMQ_ROOT NAMES include/zmq.h)
else()
set(_ZeroMQ_ROOT "${ZeroMQ_ROOT}")
endif()
find_path(ZeroMQ_INCLUDE_DIRS NAMES zmq.h HINTS ${_ZeroMQ_ROOT}/include)
if(ZeroMQ_INCLUDE_DIRS)
set(_ZeroMQ_H ${ZeroMQ_INCLUDE_DIRS}/zmq.h)
function(_zmqver_EXTRACT _ZeroMQ_VER_COMPONENT _ZeroMQ_VER_OUTPUT)
set(CMAKE_MATCH_1 "0")
set(_ZeroMQ_expr "^[ \t]*#define[ \t]+${_ZeroMQ_VER_COMPONENT}[ \t]+([0-9]+)$")
file(STRINGS "${_ZeroMQ_H}" _ZeroMQ_ver REGEX "${_ZeroMQ_expr}")
string(REGEX MATCH "${_ZeroMQ_expr}" ZeroMQ_ver "${_ZeroMQ_ver}")
set(${_ZeroMQ_VER_OUTPUT} "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()
_zmqver_EXTRACT("ZMQ_VERSION_MAJOR" ZeroMQ_VERSION_MAJOR)
_zmqver_EXTRACT("ZMQ_VERSION_MINOR" ZeroMQ_VERSION_MINOR)
_zmqver_EXTRACT("ZMQ_VERSION_PATCH" ZeroMQ_VERSION_PATCH)
# We should provide version to find_package_handle_standard_args in the same format as it was requested,
# otherwise it can't check whether version matches exactly.
if(ZeroMQ_FIND_VERSION_COUNT GREATER 2)
set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}.${ZeroMQ_VERSION_PATCH}")
else()
# User has requested ZeroMQ version without patch part => user is not interested in specific patch =>
# any patch should be an exact match.
set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
endif()
if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
find_library(ZeroMQ_LIBRARIES
NAMES zmq HINTS
${_ZeroMQ_ROOT}/lib
${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
)
else()
find_library(ZeroMQ_LIBRARIES
NAMES libzmq
"libzmq-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
"libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
libzmq_d
"libzmq-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
"libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
HINTS
${_ZeroMQ_ROOT}/lib
)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZeroMQ
FOUND_VAR
ZeroMQ_FOUND
REQUIRED_VARS
ZeroMQ_INCLUDE_DIRS
ZeroMQ_LIBRARIES
VERSION_VAR
ZeroMQ_VERSION
)
if(NOT ZeroMQ_ROOT)
set(ZeroMQ_ROOT "$ENV{ZeroMQ_ROOT}")
endif()
If the ZeroMQ_ROOT variable is not set, it attempts to retrieve the path from the environment variable and set it to the ZeroMQ_ROOT variable. This design allows us to specify the path to the ZeroMQ library by setting the environment variable without modifying the CMakeLists.txt file.
if(NOT ZeroMQ_ROOT)
find_path(_ZeroMQ_ROOT NAMES include/zmq.h)
else()
set(_ZeroMQ_ROOT "${ZeroMQ_ROOT}")
endif()
find_path(ZeroMQ_INCLUDE_DIRS NAMES zmq.h HINTS ${_ZeroMQ_ROOT}/include)
This is used to find the header file path for ZeroMQ library and store the path in the variable ZeroMQ_INCLUDE_DIRS. It first checks if the ZeroMQ_ROOT variable is defined; if not, it attempts to find the zmq.h header file in the system paths; if defined, it directly uses the value of the ZeroMQ_ROOT variable.
set(_ZeroMQ_H ${ZeroMQ_INCLUDE_DIRS}/zmq.h)
function(_zmqver_EXTRACT _ZeroMQ_VER_COMPONENT _ZeroMQ_VER_OUTPUT)
set(CMAKE_MATCH_1 "0")
set(_ZeroMQ_expr "^[ \t]*#define[ \t]+${_ZeroMQ_VER_COMPONENT}[ \t]+([0-9]+)$")
file(STRINGS "${_ZeroMQ_H}" _ZeroMQ_ver REGEX "${_ZeroMQ_expr}")
string(REGEX MATCH "${_ZeroMQ_expr}" ZeroMQ_ver "${_ZeroMQ_ver}")
set(${_ZeroMQ_VER_OUTPUT} "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()
_zmqver_EXTRACT("ZMQ_VERSION_MAJOR" ZeroMQ_VERSION_MAJOR)
_zmqver_EXTRACT("ZMQ_VERSION_MINOR" ZeroMQ_VERSION_MINOR)
_zmqver_EXTRACT("ZMQ_VERSION_PATCH" ZeroMQ_VERSION_PATCH)
If the header file is successfully found, ZeroMQ_INCLUDE_DIRS is set to its location. We continue to find the corresponding version of the ZeroMQ library using string operations and regular expressions.
if(ZeroMQ_FIND_VERSION_COUNT GREATER 2)
set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}.${ZeroMQ_VERSION_PATCH}")
else()
set(ZeroMQ_VERSION "${ZeroMQ_VERSION_MAJOR}.${ZeroMQ_VERSION_MINOR}")
endif()
This prepares the ZeroMQ_VERSION variable for find_package_handle_standard_args.
if(NOT ${CMAKE_C_PLATFORM_ID} STREQUAL "Windows")
find_library(ZeroMQ_LIBRARIES
NAMES
zmq
HINTS
${_ZeroMQ_ROOT}/lib
${_ZeroMQ_ROOT}/lib/x86_64-linux-gnu
)
else()
find_library(ZeroMQ_LIBRARIES
NAMES
libzmq
"libzmq-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
"libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
libzmq_d
"libzmq-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
"libzmq-${CMAKE_VS_PLATFORM_TOOLSET}-mt-gd-${ZeroMQ_VERSION_MAJOR}_${ZeroMQ_VERSION_MINOR}_${ZeroMQ_VERSION_PATCH}"
HINTS
${_ZeroMQ_ROOT}/lib
)
endif()
This uses the find_library command to search for the ZeroMQ library. Since the naming of the libraries differs, we need to distinguish between Unix platforms and Windows platforms.
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZeroMQ
FOUND_VAR
ZeroMQ_FOUND
REQUIRED_VARS
ZeroMQ_INCLUDE_DIRS
ZeroMQ_LIBRARIES
VERSION_VAR
ZeroMQ_VERSION
)
Finally, the standard FindPackageHandleStandardArgs.cmake is included, and the corresponding CMake command is called. If all required variables are found and the version matches, the ZeroMQ_FOUND variable is set to TRUE.
✦
Appendix
✦
find-module typically follows a specific pattern:
-
Check if the user has provided a custom location for the required package.
-
Use commands from the
find_family to search for the required components of the package, such as header files, libraries, executables, etc. We usefind_pathto find the full path of the header file andfind_libraryto find the library.CMakealso providesfind_file,find_program, andfind_package. The parameters of these commands are illustrated as follows:
find_path(<VAR> NAMES name PATHS paths)
If the search is successful, <VAR> will store the search result; if the search fails, it will be set to <VAR>-NOTFOUND. NAMES and PATHS are the names of the files that CMake should look for and the paths that the search should point to, respectively.
From the preliminary search results, the version number can be extracted. In this example, the ZeroMQ header file contains the library version, which can be extracted using string operations and regular expressions.
Finally, the find_package_handle_standard_args command is called. It handles the REQUIRED, QUIET, and version parameters of the find_package command and sets the ZeroMQ_FOUND variable.
In summary, there are four ways to find dependency packages.
-
Use
CMakefiles provided by the package vendor, such as<package>Config.cmake,<package>ConfigVersion.cmake, and<package>Targets.cmake, which are usually searched in the standard installation locations of the package. -
Use
find-modulefor the required package, whether provided byCMakeor third parties. -
Use
pkg-config, as demonstrated in this article. -
If none of these work, write your own
findmodule.
These four options are ranked by relevance, and each method has its challenges.
Currently, not all package vendors provide CMake Find files, but it is becoming increasingly common. Exporting CMake targets makes it easier for third-party code to use the libraries and/or programs it depends on.
From the beginning, find-module has been the mainstream method for locating dependencies in CMake. However, most of them still rely on setting variables used by dependencies, such as Boost_INCLUDE_DIRS, PYTHON_INTERPRETER, etc. This approach makes it difficult to ensure that dependencies are satisfied when third parties release their own packages.
The method using pkg-config adapts well because it has become the standard for Unix systems. However, for this reason, it is not a completely cross-platform method. Additionally, as stated in the CMake documentation, in some cases, users may inadvertently override the detection of packages, leading to incorrect information provided by pkg-config.
The final method is to write your own find module script, as demonstrated in this example. This is feasible and relies on FindPackageHandleStandardArgs.cmake. However, writing a comprehensive find module script is no easy task and requires consideration of many possibilities.
Finally, I wish everyone to become stronger!

