Do cyborgs dream of bionic sheep?
  • Home
  • Categories
  • Tags
  • About

>> Home / tutorial / cpp, software-tools

My CMake universe

∵ Mari-chan ∴ 2023-11-22 ∞ 10'

This is part of a series where I write down things I know or I've learned while trying to keep myself sharp in various subjects that I don't use so often anymore.

First, you might be confused about Make and CMAKE, like I was when I learned about both. Make is a much simpler system that’s not cross platform compatible. CMAKE is also just a better system at handling complex build systems, it also contains robust mechanisms to locate libraries and executables. CMake is primarily a build system generator, not a build tool itself. It sets up the environment for building but delegates the actual compilation and installation to native build tools (like Make).

  • List all of the commands and a short description about them organized by phase they should be added in your CMake file:

    Project declaration:

    • project: Sets the name and version of the project. This command also sets useful variables like PROJECT_NAME, PROJECT_VERSION, and more.

    • cmake_minimum_required: Sets the minimum required version of CMake for a project, ensuring compatibility and proper interpretation of the CMake scripts.

    • **target_compile_features**: You will also need to set the language version you’re using.

      target_compile_features(particles PUBLIC cxx_std_11)
      

    Global variable declaration and flags:

    • set: Used to define and set CMake variables that control various aspects of the build process.
    • option: Introduces a boolean option that the user can control from the command line. It's useful for conditional compilation or including optional features.

    Include directories:

    • include_directories: Is used to add directory paths to the list of directories that the compiler will search for include files during the compilation process.
      • It’s the same as running cmake -I path/to/dir. It simply adds a directory for the CMAKE to bundle header files in there with source files.

    Find dependencies:

    • find_package: Searches for and locates external libraries or packages, ensuring that the necessary dependencies are included in the build process. It's key for managing external dependencies.

      • In order to create your own find_package function you can just write a normal CMAKE file in which you. First you add the path to library, then you find the library itself, name it, then you include and handle any dependencies this library has (and add them to the CMAKE file) and then you can finally create a library with the the add_library function.
      find_path(MY_LIB_INCLUDE_DIR MyLib.h)
      find_library(MY_LIB_LIBRARY NAMES mylib)
      
      include(FindPackageHandleStandardArgs)
      find_package_handle_standard_args(MyLib DEFAULT_MSG MY_LIB_INCLUDE_DIR MY_LIB_LIBRARY)
      
      if(MY_LIB_FOUND)
        add_library(MyLib::MyLib UNKNOWN IMPORTED)
        set_target_properties(MyLib::MyLib PROPERTIES
          IMPORTED_LOCATION "${MY_LIB_LIBRARY}"
          INTERFACE_INCLUDE_DIRECTORIES "${MY_LIB_INCLUDE_DIR}")
      endif()
      
      • include: the primary functionality of include is to include other CMAKE scripts into the code.
      • but you don’t need to create your own find_package every time, if the library is simple enough you can just add a few lines of code into your current CMAKE. In the following case we find the library and link it together with our executable target_link_libraries.
      include_directories("/path/to/library/include")
      link_directories("/path/to/library/lib")
      add_executable(my_app main.cpp)
      target_link_libraries(my_app libname)
      
      • It might seem counterintuitive to configure and then link things, but that’s just how CMAKE good standards go.
      • REQUIRED key word: will throw an error if package not found
      • MODULE key word: find_package searches for pkgs in two ways: through modules and through config. The config way it expects a FooConfig.cmake file that will look for a proper cmake configuration file and the module way is the usual way that packages that do not support CMAKE natively use and you have written it yourself something like Foo.cmake
      • CONFIG: as explained in the previous point will look for a config file, which are simpler. Usually they can be automatically configured, for example.
      • If neither the CONFIG nor MODULE keywords are available than that means that CMAKE will try to look for pkgs both ways.

    Add executable or library targets:

    • **add_executable**: creates an executable from the specified source files. This executable is the final binary program that runs on a computer. It typically defines the main entry point for a program. In a build system you can have multiple targets, the add_executable specifically identifies one of the targets (libs, modules, tests…) as the executable, making it clear which component is the final runnable output. If you have different executables for different operational systems then you have it indicated through flags:

      cmake_minimum_required(VERSION 3.10)
      project(MyProject)
      
      # Check the operating system
      if(WIN32)
          # Windows-specific build
          add_executable(MyExecutable_win main_windows.cpp)
          # Additional Windows-specific configuration here
      elseif(APPLE)
          # macOS-specific build
          add_executable(MyExecutable_mac main_macos.cpp)
          # Additional macOS-specific configuration here
      elseif(UNIX AND NOT APPLE)
          # Linux/Unix-specific build
          add_executable(MyExecutable_unix main_unix.cpp)
          # Additional Unix-specific configuration here
      else()
          message(FATAL_ERROR "Unsupported operating system")
      endif()
      
      # Common configuration that applies to all executables
      # ...
      
    • **add_library**: this builds a library from the source code. A library is a collection of pre-compilled code that can be reused in various program. This function will typically generate either static or shared(dynamic) libraries.

      • Here’s a note on how the memory is divided between different software using dynamic libraries: the code itself is shared but because each process has their own memory when the processes need access to variables and such then the OS creates copies of those (no global state, etc) is shared by different processes

    Link libraries:

    • target_link_libraries: is used to specify which libraries an executable target or another library should link against during the linking phase of the build process. When building an executable or a library that depends on other libraries, you need to link these libraries to your target. target_link_libraries is used for this purpose. It ensures that the linker knows which libraries to link to your target during the build process.

      target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>...)
      
      • Why use it? There’s a bunch of goodies that makes your life much easier:
        • It automatically handles the necessary compiler and liker flags for you
        • It deals with dependency management
        • If you use the PUBLIC or INTERFACE flags it will retroactively deal with the dependencies for you
      • The PRIVATE, PUBLIC, and INTERFACE flags relate to how you want to manage the visibility of the propagation of link dependencies and include paths in the project.
        • PRIVATE: The linked libraries are used by the target, but their link dependencies and include paths are not propagated to other targets.
        • PUBLIC: They are propagated to the projects that are using this library
        • INTERFACE : Is meant for when you’re not compiling the target into a library but you still want the libraries that connect to it to be able to find and inherit its dependencies.

    Set target properties:

    • **set_target_properties**: Can configure how the target (typically an executable or a shared library) finds its dependencies at runtime. It uses the RPATH variable for that. But it’s also used to a myriad of things: set properties can control various aspects of how the target is built and linked, such as compiler options, definitions, include directories, and more.

      set_target_properties(my_target PROPERTIES CXX_STANDARD 17)
      set_target_properties(my_target PROPERTIES COMPILE_DEFINITIONS "DEBUG=1")
      set_target_properties(my_target PROPERTIES INCLUDE_DIRECTORIES "/path/to/includes")
      set_target_properties(my_target PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra;-O2")
      set_target_properties(my_target PROPERTIES OUTPUT_NAME "my_custom_name")
      set_target_properties(my_target PROPERTIES LINK_LIBRARIES "lib1;lib2")
      
      • Just as a note -Wall and -Wextra are turning on warnings and -02 is an optimization flag that will try to hit a balance between space and speed for the result of the compilation.
      • These are going to override global settings.
    • target_compile_definitions and target_compile_options: These commands add preprocessor definitions and compile options to a target, respectively. They offer fine-grained control over the compilation process. But they’re kind of like set_target_properties.

    • target_include_directories: The main difference between include_directories and target_include_directories is that the first one has a global scope, while the later sets the scope for a specific target. This should be used in more complex projects in which you have several targets with different include requirements.

    Add custom commands and configurations:

    • add_custom_command and add_custom_target: These commands are used to add custom build steps and targets, allowing the execution of custom commands during the build process.

      • Ways of adding custom command. You can run scripts like so:
      add_custom_target(
          generate_docs
          COMMAND doxygen Doxyfile
          COMMENT "Generating documentation with Doxygen"
      )
      
      • It’s a kind of bash script within CMAKE, it will add a step to your make, for example with the previous case you can now run make generate_docs and this piece of code will run.
    • ExternalProject_Add: Adds a step to download, update, and build an external project. It's useful for integrating external code bases that are not part of the main project.

      ExternalProject_Add(name
          [GIT_REPOSITORY <git_repo_url>]
          [GIT_TAG <git_tag>]
          [SOURCE_DIR <source_dir>]
          [BINARY_DIR <binary_dir>]
          [CONFIGURE_COMMAND <command>]
          [BUILD_COMMAND <command>]
          [INSTALL_COMMAND <command>]
          [...other options...]
      )
      
      • It’s a command that can handle the entire lifecycle of an external project.
    • add_subdirectory: Adds a subdirectory to the build. This is essential for structuring larger projects with multiple components or libraries.

    • configure_file: Copies and processes a file, replacing variable references with their current values. Useful for generating configuration headers and similar tasks.

      • Allows you to add variables to your files, like versioning for example.
    • file: Provides various file manipulation capabilities, such as generating a list of source files, copying files, etc.

    Install:

    • install: Specifies how and where files should be installed on the system when you run the make install command (or the installation step of your chosen build system).

      # Create an executable target
      add_executable(my_app src/main.cpp)
      
      # Define where to install the executable
      install(TARGETS my_app DESTINATION bin)
      
      # Create a library target
      add_library(my_lib SHARED src/my_lib.cpp)
      
      # Define where to install the library
      install(TARGETS my_lib LIBRARY DESTINATION lib)
      
      # Install header files
      install(DIRECTORY include/ DESTINATION include)
      
      • TARGETS: Used to install targets, like executables or libraries. You specify the actual target names and where they should be installed (DESTINATION). The destination is usually relative to the installation prefix, which is typically /usr/local on Unix systems but can be set to a different value.
      • LIBRARY, RUNTIME, ARCHIVE: Specify where different types of targets should be installed. LIBRARY is for shared libraries, RUNTIME for executables and DLLs, and ARCHIVE for static libraries.
      • FILES: Used to install individual files. You specify the files and their destination.
      • DIRECTORY: Used to install directories of files. It copies the contents of specified directories to a destination directory.
      • CONFIGURATIONS: Optionally, you can specify configurations (like Debug, Release) for which the install rules apply.
      • PERMISSIONS: You can set file permissions for Unix systems.
      • COMPONENT: Used for component-based installations, allowing users to select which components to install.

    Test:

    • enable_testing and add_test: These commands are used to enable and add tests to a project. They integrate with CTest for managing test suites.

    Package:

    • CPack: CMake's packaging system, which is used to create installers and distributable packages for a project.
      • It’s used to create binaries and source pkgs. After running your project with CMAKE you run CPACK to generate the pkgs.

    Others:

    • **macro vs function**: macros don’t have a defined scope, while functions do.
    • message: Generates a message during the CMake run. Useful for debugging or providing build-time information.

Search

Categories

  • activism
  • journal
  • research
  • tutorial

Tags

  • ai
  • art
  • books
  • community
  • counter-culture
  • cpp
  • data
  • design
  • electronics
  • js
  • learning
  • lisp
  • love
  • performance
  • privacy
  • python
  • rust
  • security
  • software-tools
  • web-dev

2023 © Mari-chan | Twitter Linkedin GitHub | Built on Zola