Tag: cmake

  • Mastering CPM: 6 Tips for Effectively Managing Dependencies with CMake’s Package Manager

    Include the right version right in your repo; provide a custom command to update CPM.cmake — example

    Setup cmake_modules path — example CMake

    Best Practices

    • Avoid short hand
    • Keep package dependencies with their targets

    CMake is a cross-platform build system that can be used to build, test, and package software projects. One of the most powerful features of CMake is the ability to manage dependencies, and using CMake Package Manager (CPM) make that a breeze. CPM is a package manager for CMake that allows you to easily download and use libraries and other dependencies in your project without having to manually manage them.

    Using CPM effectively can greatly simplify the process of managing dependencies in your project. Here are a few tips to help you get the most out of CPM:

    1. Start by adding CPM to your project. This can be done by adding the following line to the top of your CMakeLists.txt file (note that you will need to have a cmake/CPM.cmake in that path relative to your CMakeLists.txt file). You can find up-to-date versions of CPM.cmake here (documentation is here).
    include(cmake/CPM.cmake)Code language: PHP (php)
    1. Next, specify the dependencies you need for your project by adding CPMAddPackage commands. For example, to add the msgpack-c library, you would add the following stanza:
    CPMAddPackage(
      NAME msgpack
      GIT_TAG cpp-4.1.1
      GITHUB_REPOSITORY "msgpack/msgpack-c"
      OPTIONS "MSGPACK_BUILD_DOCS OFF" "MSGPACK_CXX20 ON" "MSGPACK_USE_BOOST OFF"
    )
    Code language: JavaScript (javascript)
    1. Once you have added all the dependencies you require, you can use them in your project by including the appropriate headers and linking against the appropriate libraries. Note that CPM will pull the dependency from the repository specified and then run CMake (if the project uses CMake to build). Because CMake is run for the project, the CMake targets are available for you to use. For example, to use the msgpack-c library in your project, you would add the following lines to your CMakeLists.txt file:
    target_link_libraries(your_target msgpackc-cxx)
    1. CPM also allows you to specify options for modules, such as disabling tests or building in a specific configuration. To specify options for a module, you can use the OPTIONS argument, as shown above.
    2. When the dependency does not have a CMakeLists.txt file, CPM will still checkout the repository, but will not configure it. In that case, you are required to write your own CMake to perform the build as required. For example, the embedded web server called Mongoose from Cesanta does not provide a CMakeLists.txt to build it, but we can still pull it in like this (note the use of the CPM generated variable mongoose_SOURCE_DIR):
    CPMAddPackage(
      NAME mongoose
      GIT_TAG 7.8
      GITHUB_REPOSITORY "cesanta/mongoose"
    )
    if (mongoose_ADDED)
      add_library(mongoose SHARED ${mongoose_SOURCE_DIR}/mongoose.c)
      target_include_directories(mongoose SYSTEM PUBLIC ${mongoose_SOURCE_DIR})
      target_compile_definitions(mongoose PUBLIC MG_ENABLE_OPENSSL)
      target_link_libraries(mongoose PUBLIC ssl crypto)
      install(TARGETS mongoose)
    endif(mongoose_ADDED)Code language: PHP (php)
    1. Add dependencies with CPM in the same CMakeLists.txt as the target that uses the dependency. If multiple targets use the same dependency, CPM will not pull multiple copies, rather it will use the copy already downloaded. By doing this, you ensure that if you ever refactor your CMake, or pull a CMakeLists.txt for a module, you get all the dependencies and don’t miss anything.

    CPM is a powerful tool that can help simplify the process of managing dependencies in your project. By following the tips outlined in this blog, you can effectively use CPM to manage your dependencies, ensuring that your project is always up-to-date and easing the burden of keeping your dependencies up-to-date as well. With the right approach, CPM can help you save time and effort when managing your project dependencies, allowing you to focus on building and delivering your project.

  • Getting Started with CMake: A Beginner’s Guide to Building Your Project

    CMake is an open-source, cross-platform build system that helps developers to manage their projects and build them on different platforms. It is widely used in the software development community, especially for C and C++ projects. In this blog post, we will explore how to use CMake effectively to manage your projects and improve your workflow as a software developer.

    An Example CMakeLists.txt

    First, let’s start with the basics of CMake. CMake uses a simple, human-readable language called CMakeLists.txt to describe the build process of a project. This file contains instructions on how to find and configure dependencies, set compiler flags, and create the final executable or library. Here is an example of how I typically define my CMake from my open-source ZeroMQ-based RPC library.

    ###############################################################################
    # CMakeLists.txt for zRPC library
    #  - Creates a CMake target library named 'zRPC'
    ###############################################################################
    cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
    
    # Define the project, including its name, version, and a brief description
    project(zRPC
            VERSION "0.0.1"
            DESCRIPTION "0MQ-based RPC client/server library with MessagePack support"
           )
    
    # Define CMake options to control what targets are generated and made available to build
    option(ZRPC_BUILD_TESTS "Enable build of unit test applications" ON)
    
    # Setup default compiler flags
    set(CMAKE_C_STANDARD 11)
    set(CMAKE_C_STANDARD_REQUIRED ON)
    set(CMAKE_CXX_STANDARD 20)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    set(compile_options -pedantic-errors
                        -pedantic
                        -Wall
                        -Wextra
                        -Wconversion
                        -Wsign-conversion
                        -Wno-psabi
                        -Werror
        CACHE INTERNAL "Compiler Options"
       )
    
    ###############################################################################
    # Bring in CPM
    ###############################################################################
    include(cmake/CPM.cmake)
    
    ###############################################################################
    # Bring in CPPZMQ header-only API
    ###############################################################################
    CPMAddPackage(
      NAME cppzmq
      VERSION 4.8.1
      GITHUB_REPOSITORY "zeromq/cppzmq"
      OPTIONS "CPPZMQ_BUILD_TESTS OFF"
    )
    
    ###############################################################################
    # Bring in MSGPACK-C header-only API
    ###############################################################################
    CPMAddPackage(
      NAME msgpack
      GIT_TAG cpp-4.1.1
      GITHUB_REPOSITORY "msgpack/msgpack-c"
      OPTIONS "MSGPACK_BUILD_DOCS OFF" "MSGPACK_CXX20 ON" "MSGPACK_USE_BOOST OFF"
    )
    
    ###############################################################################
    # Bring in C++ CRC header-only API
    ###############################################################################
    CPMAddPackage(
      NAME CRCpp
      GIT_TAG release-1.1.0.0
      GITHUB_REPOSITORY "d-bahr/CRCpp"
    )
    if(CRCpp_ADDED)
      add_library(CRCpp INTERFACE)
      target_include_directories(CRCpp SYSTEM INTERFACE ${CRCpp_SOURCE_DIR}/inc)
    endif(CRCpp_ADDED)
    
    ###############################################################################
    # zRPC library
    ###############################################################################
    add_library(${PROJECT_NAME} SHARED)
    target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
    target_link_libraries(${PROJECT_NAME} PUBLIC cppzmq msgpackc-cxx CRCpp pthread)
    target_sources(${PROJECT_NAME} PRIVATE  src/zRPCClient.cpp
                                            src/zRPCServer.cpp
                                            src/zRPCPublisher.cpp
                                            src/zRPCSubscriber.cpp
                                   PUBLIC   include/zRPC.hpp
                  )
    target_compile_options(${PROJECT_NAME} PUBLIC ${compile_options})
    
    
    ###############################################################################
    # Test applications
    ###############################################################################
    if (ZRPC_BUILD_TESTS)
      add_executable(client tests/client.cpp)
      target_link_libraries(client zRPC)
      target_compile_options(client PUBLIC ${compile_options})
    
      add_executable(server tests/server.cpp)
      target_link_libraries(server zRPC)
      target_compile_options(server PUBLIC ${compile_options})
    
      add_executable(publisher tests/publisher.cpp)
      target_link_libraries(publisher zRPC)
      target_compile_options(publisher PUBLIC ${compile_options})
    
      add_executable(subscriber tests/subscriber.cpp)
      target_link_libraries(subscriber zRPC)
      target_compile_options(subscriber PUBLIC ${compile_options})
    
      include(cmake/CodeCoverage.cmake)
      append_coverage_compiler_flags()
      add_executable(unittest tests/unit.cpp)
      target_link_libraries(unittest zRPC)
      target_compile_options(unittest PUBLIC ${compile_options})
    
      setup_target_for_coverage_gcovr_xml(NAME ${PROJECT_NAME}_coverage
                                          EXECUTABLE unittest
                                          DEPENDENCIES unittest
                                          BASE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                                          EXCLUDE "tests"
    				                             )
    endif(ZRPC_BUILD_TESTS)
    Code language: CMake (cmake)

    Once you have your CMakeLists.txt file created, you can use the CMake command-line tool to generate the build files for a specific platform, such as Makefiles or Visual Studio project files. It is considered best practice to keep your build files separated from your source files, so I am in the habit of creating a “_bld” directory for that purpose.

    mkdir _bld; cd _bld
    cmake ..

    CMake Targets

    Targets are the basic building blocks of a CMake project. They represent the executable or library that is built as part of the project. Each target has a unique name and is associated with a set of source files, include directories, and libraries that are used to build it.

    CMake also supports creating custom targets, which can be used to run arbitrary commands as part of the build process, such as running tests or generating documentation. You can specify properties for the target, like include directories, libraries, or compile options. You can also specify dependencies between the targets, so that when one target is built, it will automatically build any targets it depends on.

    This is a really powerful feature that CMake provides because when I define my library target, I define what it needs to build such as the source files, includes, and external libraries. Then, when I define my executable, I only need to specify the library that it depends on — the requisite includes and other libraries that need to be linked in come automatically!

    add_library(${PROJECT_NAME} SHARED)
    target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
    target_link_libraries(${PROJECT_NAME} PUBLIC cppzmq msgpackc-cxx CRCpp pthread)
    target_sources(${PROJECT_NAME} PRIVATE  src/zRPCClient.cpp
                                            src/zRPCServer.cpp
                                            src/zRPCPublisher.cpp
                                            src/zRPCSubscriber.cpp
                                   PUBLIC   include/zRPC.hpp
                  )
    target_compile_options(${PROJECT_NAME} PUBLIC ${compile_options})
    
    # The executable only needs to depend on zRPC now,
    # not all the other dependencies and include directories
    add_executable(client tests/client.cpp)
    target_link_libraries(client zRPC)Code language: PHP (php)

    Dependency Management

    One of the most important aspects of CMake is its ability to help you find and use dependencies. CMake provides a number of built-in commands that can be used to find and configure dependencies, such as find_package and find_library. These commands can be used to locate and configure external libraries, such as Boost or OpenCV, and make them available to your project. This can save you a lot of time and effort compared to manually configuring dependencies for each platform, which is how it was done with plain Makefiles in the past.

    In my example above, I use a tool called CPM, or the CMake Package Manager. This is an abstraction of the find_package and find_library methods available in the CMake language. One huge advantage of this tool is that it can not only be used to find and use local packages, but it can be used to pull packages at a specific version or tag from remote git repositories. You can see how I used this to pull in the cppzmq, msgpack, and CRCpp packages that my library depends on.

    ###############################################################################
    # Bring in CPM
    ###############################################################################
    include(cmake/CPM.cmake)
    
    ###############################################################################
    # Bring in CPPZMQ header-only API
    ###############################################################################
    CPMAddPackage(
      NAME cppzmq
      VERSION 4.8.1
      GITHUB_REPOSITORY "zeromq/cppzmq"
      OPTIONS "CPPZMQ_BUILD_TESTS OFF"
    )
    
    ###############################################################################
    # Bring in MSGPACK-C header-only API
    ###############################################################################
    CPMAddPackage(
      NAME msgpack
      GIT_TAG cpp-4.1.1
      GITHUB_REPOSITORY "msgpack/msgpack-c"
      OPTIONS "MSGPACK_BUILD_DOCS OFF" "MSGPACK_CXX20 ON" "MSGPACK_USE_BOOST OFF"
    )
    
    ###############################################################################
    # Bring in C++ CRC header-only API
    ###############################################################################
    CPMAddPackage(
      NAME CRCpp
      GIT_TAG release-1.1.0.0
      GITHUB_REPOSITORY "d-bahr/CRCpp"
    )
    if(CRCpp_ADDED)
      add_library(CRCpp INTERFACE)
      target_include_directories(CRCpp SYSTEM INTERFACE ${CRCpp_SOURCE_DIR}/inc)
    endif(CRCpp_ADDED)
    Code language: CMake (cmake)

    Cross-Platform Build Support

    Another powerful feature of CMake is its ability to generate build files for multiple platforms. For example, you can use CMake to generate Makefiles for Linux, Visual Studio project files for Windows, or Xcode project files for macOS. This allows you to easily build and test your project on different platforms, without having to manually configure the build process for each one.

    # Basic command to generate Makefiles (Linux/MacOS)
    cmake -G "Unix Makefiles" ..
    
    # Basic command to generate Visual Studio build files
    cmake -G "Visual Studio 16" -A x64 ..
    
    # More complex command from the VS Code CMake extension performing cross-compilation for ARM
    /usr/bin/cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE \
        -DCMAKE_BUILD_TYPE:STRING=Release 
        -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/arm-linux-gnueabihf-gcc-10 
        -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/arm-linux-gnueabihf-g++-10 
        -DARCH:STRING=armv7 -DENABLE_TESTS:STRING=ON 
        -S/workspaces/zRPC -B/workspaces/zRPC/_bld/ARM/Release 
        -G "Unix Makefiles"Code language: PHP (php)

    Best Practices

    To improve your workflow with CMake, there are a few best practices that you should follow:

    • Keep your CMakeLists.txt files small and organized. The build process of a project can become complex, so it’s important to keep your CMakeLists.txt files well-organized and easy to understand.
    • Use variables to define common build options, such as compiler flags or library paths. This makes it easy to change these options globally, without having to modify multiple parts of your CMakeLists.txt files.
    • Use include() and add_subdirectory() commands to split your project into smaller, more manageable parts. This makes it easier to understand the build process, and also makes it easy to reuse parts of your project in other projects. I have found that many, small CMake files are easier to manage and maintain than fewer, large CMake files.
    • Use the install() command to specify where the final executable or library should be installed. This makes it easy to distribute your project to other users.
    • Use the add_custom_command() and add_custom_target() commands to add custom build steps to your project. For example, you can use these commands to run a script that generates source code files or to run a test suite after building.

    By following these best practices, you can effectively use CMake to manage your projects and improve your workflow as a software developer. CMake is a powerful tool that can save you a lot of time and effort, and by mastering its features, you can build and distribute your projects with ease.