What is a build system

In software development, a build system is an automated tool used to generate targets from source code that users can use. Targets can include libraries, executables, or generated scripts, and so on.

Typically, each build system has a corresponding build file (also called a configuration file, project file, etc.) that guides the build system on how to compile, link, and produce executables. The build file usually describes the target to be generated, the source files needed to generate the target, the dependency libraries, and so on. The names and content format specifications of the build files used by different build systems generally vary.

Common build systems

  • GNU Make: Build system for UniX-like operating systems. The name of the build file is usuallyMakefileormakefile.
  • NMake: can be understood as Windows platformGNU Make, it is MicrosoftVisual StudioThe build system used by earlier versions, such asVc + + 6.0. The suffix of the build file is.mak.
  • MSBuild:NMakeThe replacement, at first, was with.netFrame bound.Visual Studiofrom2013Version started using it as a build system.Visual StudioThe project build depends onMSBuild, butMSBuildDoes not depend on the former, can run independently. The suffix of the build file is.vcproj(inC++Project as an example).
  • Ninja: a small build system focused on speed,ChromeTeam development.

CMake

CMake is an open source, cross-platform build system for managing software builds that are not dependent on a specific compiler and can support multiple levels of directories, applications, and libraries. While CMake also controls the build process with build files (the name is cMakelists.txt), it does not directly build and generate the target. Instead, it generates the build files required by other build systems, which then build and generate the final target. Support MSBuild, GNU Make, MINGW Make and many more build systems.

qmake

Qmake is one of the tools that comes with Qt that helps simplify the build process for cross-platform projects. Like CMake, Qmake does not directly build and generate targets, but relies on other build systems. It automatically generates makefiles, Visual Studio project files, and XCode project files. Since qmake can be used whether or not a project uses the Qt framework, it can be used in many software build processes.

The build files used by Qmake are. Pro project files, which can be written by developers or generated by Qmake itself. Qmake includes additional features to facilitate Qt development, such as automatically including moC and UIC compilation rules. It is worth mentioning that CMake also supports Qt development, but does not rely on Qmake.

Step By Step

In this introductory Tutorial, I mainly refer to the official Tutorial of CMake and some materials on the Internet, and make some changes and supplements to make it easier to understand.

1. Install and configure the development environment

Before we start, we can choose a favorite IDE(integrated development environment) as a C/C++ development tool. CMake supports Visual Studio, QtCreator, Eclipse, CLion and other ides. Of course, you can also use text editors like VSCode, Vim and some plug-ins as development tools. Due to my personal habits and preferences, and mainly under Linux system development, I finally chose JetBrains family of CLion as the development tool.

  • Install the build toolchain (CMake, compiler, debugger, build system) :

    Sudo apt install CMake cmake-qt-gui. If the version in apt is too low, you can manually compile and install CMake and cmake-gui. For Windows, go to the official website to download the installation program.

    Others: Use the built-in GNU suite (make, GCC, GDB) for Linux, and mingW-W64, MSVC, or WSL for Windows.

  • Download and install: CLion website, free trial 30 days.

  • First run: The login account is activated and authorized, and preferences are configured. Students can register for a JetBrains account using an EDU email address and then have free access to the ULTIMATE version of all the JetBrains family ides.

  • Interface Chinese (optional) : Square X JetBrains series software Chinese package. My English is not very good, so it is necessary to be Chinese.

  • Configure the Build toolchain (Set -> Build, Execute, Deploy -> Toolchain) : This step is to configure the path of the tools required for the build in CLion. CMake can use a version that comes bundled with CLion, or you can choose your own installed version.

  • Configure the CMake options (Set -> Build, Execute, Deploy -> CMake) : Set the build type (Debug/Release), the CMake build options parameters, the build directory, and so on. Keep the default Settings until you need to change the CMake build options.

Create a CLion C++ project

Open CLion and create a new C++ executable project. For the C++ standard version, I chose C++17. If you select the standard version and build target type when you create a new project, you can change them from the cmakelists.txt file.

3. Project structure and content analysis of cMakelists.txt

After creating a project, the initial structure looks like this:

  • CMakeLearnDemo: project source directory, the top-level directory containing project source files.

  • Main. CPP: The automatically generated source file for the main function.

  • Cmake-build-debug: The default build directory generated by CLion calling cmake. What is a build directory? A top-level directory that stores build system files (such as makefiles and other cmake-related configuration files) and build output files (intermediate files generated by compilation, executables, libraries). Because you don’t want to mess up the project structure by mixing build generated files with project source files, you usually create a separate build directory. Of course, you can use the project source directory directly as the build directory if you prefer. With CLion we don’t need to manually call CMake on the command line to generate the build directory and the build project. CLion automatically regenerates the build directory when the contents of cMakelists. TXT are changed, and the project can be built by clicking the Build button. But in the learning phase, it is important to understand the basic usage of CMake, and once you are familiar with it, you will be comfortable using an IDE. Let’s create a new myBuild directory in the project source directory and use it as the build directory when we manually invoke the CMake command, as shown in the figure below:

  • Cmakelists. TXT: cmake project configuration file, specifically the cMake configuration file in the project top-level directory, because a project can have multiple cMakelists. TXT files in multiple directories. This should be the core cmake configuration file, and it is around this file that most of the changes to the project’s build configuration are made. Let’s see what cMakelists. TXT is automatically generated for us by CLion:

    cmake_minimum_required(VERSION 3.15)
    Copy the code

    Set the minimum version requirement for cmake. If cMake runs a version below the minimum required version, it will stop processing the project and report an error.

    project(CMakeLearnDemo)
    Copy the code

    Set the name of the project and store its value in the cmake built-in variable PROJECT_NAME. When called from top-level cMakelists.txt, it is also stored in the built-in variable CMAKE_PROJECT_NAME.

    set(CMAKE_CXX_STANDARD 17)
    Copy the code

    Sets the version of the C++ standard. Currently, the supported version values are 98, 11, 14, 17 and 20.

    add_executable(CMakeLearnDemo main.cpp)
    Copy the code

    Add an executable file type build target to the project. CMakeLearnDemo is the filename followed by a list of the source files needed to generate the executable.

4. Configure, build, and run a basic project

First, let’s change the contents of main. CPP to the following code:

#include <cmath>
#include <iostream>

int main(int argc, char *argv[])
{
    if(argc < 2)
    {
        std: :cerr << "Must have at least 2 command line arguments." << std: :endl;
        return 1;
    }

    try
    {
        double inputValue = std::stof(argv[1]);
        double outputValue = std: :sqrt(inputValue);
        std: :cout << "the square root of " << inputValue 
                  << " is " << outputValue << std: :endl;
    }
    catch(const std::invalid_argument& e)
    {
        std: :cerr << e.what() << std: :endl;
        return 1;
    }

    return 0;
}
Copy the code

The main function reads the value of a command line argument, calculates its arithmetic square root, and prints it, including some errors.

You clearly need a version of the C++ standard

The previously set CMAKE_CXX_STANDARD is only an optional attribute, and if the compiler does not support this version of the standard, it is still possible to degenerate to the previous version. If we want to explicitly indicate that we need a C++ standard, we can do so by:

set(CMAKE_CXX_STANDARD_REQUIRED True)
Copy the code

To implement. In this case, if the compiler does not support the standard, CMake will simply stop running with an error.

Generate the project build system

This step can be understood as a project configuration process, and no compilation takes place. Cmake generates build files for the build system in the build directory based on the project’s cMakelists. TXT file. It also contains a number of cmake-related configuration files, but we don’t need to care about the contents of these automatically generated files right now.

The commands that generate the project build system take three forms:

Use the current directory as the build directory and as the project source directory. It could be relative or absolute.

cmake [<options>] <path-to-source>

# For example,.. Represents the parent directory of the current directory
cd mybuild
cmake ..
Copy the code

Use as the build directory and get the project source directory from its cmakecache.txt file, which must have been generated in the previous CMake runtime, that is, from the build directory that has previously generated the build system. And regenerate it. It could be relative or absolute.

cmake [<options>] <path-to-existing-build>

# for
cmake mybuild
Copy the code

Use as the project source directory and

cmake [<options>] -S <path-to-source> -B <path-to-build>

For example,. Represents the current directory
cmake -S . -B mybuild
Copy the code

I prefer the third way because it’s more intuitive. Let’s generate the project build system ourselves:

Switch build system

For each build system, there is a Cmake generator that generates the native related build files for the build system, which can be specified by -g

when calling the cmake command-line tool. It can also be specified in cmakelists.txt by setting the value of the CMAKE_GENERATOR variable.

Cmake generators are platform-specific, so each generator can only be used on a particular platform. You can use the cmake –helo command line to see what generators are available on the current platform. My Linux Mint 1903 output looks like this:

Switching build types

Build types such as Debug, Release, RelWithDebInfo, MinSizeRel, etc., can be specified with CMAKE_BUILD_TYPE variables, such as:

set(CMAKE_BUILD_TYPE Release)
Copy the code

Build the project

Once the project build system is generated, you can then choose to build the project. We can build the project directly by calling the corresponding build system, such as GNU Make, or we can call cmake to have it automatically select the corresponding build system to build the project. As follows:

Or:

Run the executable

Now that the build is complete, let’s run the executable and see what happens:

5. Add a version number for the project

It is possible to define the version number directly in the source file, but it is more flexible to set it in cmakelists.txt. The configuration version number is an optional item in the project command. The syntax is as follows:

project(<PROJECT-NAME>
				 [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
Copy the code

It is not difficult to see that there are at least 1 and at most 4 levels of version numbers. Take version 0.8.1.23 as an example. The values and meanings at different levels are as follows:

  • major: Indicates the major version number. The value is0. Can be achieved bycmakeBuilt-in variablesPROJECT_VERSION_MAJORor<PROJECT-NAME>_VERSION_MAJORTo get its value.
  • minor: Indicates the version number. The value is8. Can be achieved bycmakeBuilt-in variablesPROJECT_VERSION_MINORor<PROJECT-NAME>_VERSION_MAJORTo get its value.
  • patch: Indicates the patch version. The value is1. Can be achieved bycmakeBuilt-in variablesPROJECT_VERSION_PATCHor<PROJECT-NAME>_VERSION_PATCHTo get its value.
  • tweak: Slightly changed version number. The value is23. Can be achieved bycmakeBuilt-in variablesPROJECT_VERSION_TWEAKor<PROJECT-NAME>_VERSION_TWEAKTo get its value.
  • Full version number0.8.1.23Can be achieved bycmakeBuilt-in variablesPROJECT_VERSIONor<PROJECT-NAME>_VERSIONTo get its value.

Now, let’s create an autoGeneratedHeaders folder to hold the header files that are automatically generated or updated by cmake. Next, create a projectconfig.h.id file with the following contents:

#ifndef CMAKELEARNDEMO_PROJECTCONFIG_H
#define CMAKELEARNDEMO_PROJECTCONFIG_H

#define PROJECT_VERSION "@CPROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH  @PROJECT_VERSION_PATCH@

#endif //CMAKELEARNDEMO_PROJECTCONFIG_H
Copy the code

Next add or change the following to cmakelists.txt:

project(CMakeLearnDemo VERSION 1.0.0)

configure_file(
        autoGeneratedHeaders/projectConfig.h.in
        ${PROJECT_SOURCE_DIR}/autoGeneratedHeaders/projectConfig.h
)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
)
Copy the code

Configure_file (…) The function of the command is to copy the specified file to another location and replace something. Relative to the project source directory, if used; If the relative path is used, it is relative to the project build directory, so in the value of I used the cmake variable PROJECT_SOURCE_DIR to get the project source directory. If you want to directly replace the contents of a file, you can point the values of and to the same file.

As for substitution, in projectconfig.h.inn, the @variable_name @ syntax is a reference to the value of a cmake variable, which will be replaced by the value of the corresponding variable after configure_file. How many project levels to define is up to you, but I’ve only defined three project levels and a full version number.

target_include_directories(…) The command is to add the specified directory to the compiler search included file directory for the specified build target. You can use it directly in main.cpp by adding autoGeneratedHeaders to the include file directory

#include "projectConfig.h"
/ / or
#include <projectConfig.h>
Copy the code

without

#include "autoGeneratedHeaders/projectConfig.h"
/ / or
#include <autoGeneratedHeaders/projectConfig.h>
Copy the code

Next, let’s output the project version information in main. CPP, as follows:

#include "projectConfig.h"

int main(int argc, char *argv[])
{
    std: :cout << "Project Version: " << PROJECT_VERSION << std: :endl;
    std: :cout << "Project Version Major: " << PROJECT_VERSION_MAJOR << std: :endl;
    std: :cout << "Project Version Minor: " << PROJECT_VERSION_MINOR << std: :endl;
    std: :cout << "Project Version Patch: " << PROJECT_VERSION_PATCH << std: :endl; .Copy the code

Regenerate the build system and, unsurprisingly, add a new projectconfig.h file.

Build and run the executable to see the output:

6. Add the library

We used STD :: SQRT in main to compute the square root. Now, let’s implement our own square root function, build it into a static library, and finally use our own square root library function in main instead of the standard library.

Code implementation, library generation and use

Create a myMath folder to hold the.h,.cpp, and cmakelists.txt files for our own square root functions. The structure is as follows:

Mymath.h has the following contents:

#ifndef CMAKELEARNDEMO_MYMATH_H
#define CMAKELEARNDEMO_MYMATH_H

#include <stdexcept>

namespace mymath
{
    // calculate the square root of number
    double sqrt(double number) noexcept(false);
}

#endif // CMAKELEARNDEMO_MYMATH_H
Copy the code

Mymath.cpp contains the following:

#include "mymath.h"

namespace mymath
{
    double sqrt(double number)
    {
        static constexpr double precision = 1e-6;
        static constexpr auto abs= [] (double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        returnguess; }}Copy the code

Cmakelists.txt contains the following:

add_library(mymath STATIC mymath.cpp)
Copy the code

Add_library is similar to add_execuable, except that it adds a library target instead of an executable file target. Mymath is the library name, STATIC means to generate the STATIC library, and myMath.cpp is the source file needed to generate the library.

Next we need to add or change the following in cMakelists.txt in the project root directory:

add_subdirectory(mymath)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
        mymath
)

target_link_libraries(CMakeLearnDemo PUBLIC
        mymath
)
Copy the code

The function of add_subdirectory is to add a subdirectory containing cMakelists.txt to the build, because the child cMakelists.txt will not participate in the build if it has not been added to the root cMakelists.txt.

Add myMath to target_include_directories because you need to include the myMath. h header file in main.cpp; The function of target_link_libraries is to link the dependency libraries to the specified target, because the MyMath library is required in main.cpp.

Now let’s replace STD :: SQRT with our own square root function in main. CPP. The changes are as follows:

- #include <cmath>
+ #include "mymath.h".int main(int argc ,char* argv[])
{...double outputValue = mymath::sqrt(inputValue); . }Copy the code

Regenerate the build system and run the build. The output should be the same as before; Open myBuild directory and you will find a new subdirectory called myMath, which contains the library file libmyMath.a.

Make the library optional

Now let’s make the MyMath library user optional, which, while not necessarily necessary for a tutorial, is quite common in a large project.

First let’s add or change the following in cmakelists.txt:

option(USE_MYMATH "Use CMakeLearnDemo provided math implementation" ON)
message("value of USE_MYMATH is : " ${USE_MYMATH})

configure_file(
        autoGeneratedHeaders/projectConfig.h.in
        ${PROJECT_SOURCE_DIR}/autoGeneratedHeaders/projectConfig.h
)

if(USE_MYMATH)
    add_subdirectory(mymath)
    list(APPEND EXTRA_INCLUDES mymath)
    list(APPEND EXTRA_LIBS mymath)
endif(a)add_executable(CMakeLearnDemo main.cpp)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
        ${EXTRA_INCLUDES}
)

target_link_libraries(CMakeLearnDemo PUBLIC
        ${EXTRA_LIBS}
)
Copy the code

Explanation:

option(<variable> "<help_text>" [value])
Copy the code

Add an option for the user to select ON or OFF. The final value is stored in the

. ”

The message command outputs a message when the build system is generated, usually to see the value of a variable or something like that.

The if() and endif() conditional statements work just like they do in other high-level languages. You can read the cmake if syntax to see which variable values will be True if and which will be False.

The column phenotype variable in cmake refers to the variable used by; Delimited strings, such as “a; b; c; d; E “. The list() command performs a series of operations on the column phenotype variables, such as add, insert, delete, get length, and so on. In this tutorial, we use the variables EXTRA_INCLUDES and EXTRA_LIBS to store separate include and library paths for optional libraries selected by the user.

To create a column phenotype variable, use the set() command. Try adding the following lines to cMakelists.txt and observe the output:

set(testVariable "a" "b" "c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})

set(testVariable "a; b; c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})

set(testVariable "a b c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})
Copy the code

Next, let’s add the following line to projectconfig.h.id:

#cmakedefine USE_MYMATH
Copy the code

The configure_file command replaces this line with the value of the USE_MYMATH variable. If the value is True by the if command, it will be replaced with:

#define USE_MYMATH
Copy the code

Otherwise, it will be replaced with:

/* #undef USE_MYMATH */
Copy the code

This way, we can choose to use the standard library or our own library in our code by using conditional compilation to determine whether the USE_MYMATH macro is defined. In main. CPP we need to make the following changes:

#ifdef USE_MYMATH
#include "mymath.h"
#else
#include <cmath>
#endif.int main(int argc ,char* argv[])
{...#ifdef USE_MYMATH
        double outputValue = mymath::sqrt(inputValue);
#else
        double outputValue = std: :sqrt(inputValue);
#endif. }Copy the code

Add usage requirements for the library

Using requirements provides better control in CMake over the linking and inclusion directories of libraries or executables, as well as the passing of properties between build targets. The main commands that affect usage requirements are as follows:

  • target_compile_definitions
  • target_compile_options
  • target_include_directories
  • target_link_libraries

Now let’s refactor our code to add usage requirements using the modern CMake method. We first require that any build target linked to the MyMath library include the MyMath directory, which the MyMath library itself does not, of course, so this could be an INTERFACE usage requirement.

Interfaces are requirements that consumers (other build targets that use the library) need but producers (the library itself) do not. Now let’s add the following to myMath/cMakelists.txt:

target_include_directories(mymath
        INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
Copy the code

The value of the CMAKE_CURRENT_SOURCE_DIR variable is the full path to the source directory currently being processed by CMake, which in this case is the myMath directory. This way, all build targets linked to the MyMath library automatically include the myMath directory, and EXTRA_INCLUDES can now be safely removed:

if(USE_MYMATH)
    add_subdirectory(mymath)
# Delete list(APPEND EXTRA_INCLUDES myMath)
    list(APPEND EXTRA_LIBS mymath)
endif()

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
# remove ${EXTRA_INCLUDES}
)
Copy the code

Use cmake-GUI to configure the project and generate the build system

Our project now has a build option, and the GUI graphical interface makes the build option more straightforward for the user, so instead of using the command line cmake, we use cmake-GUI to configure the project and generate the build system.

To open cmake-gui, first select the project source directory and the build directory, as shown in the figure:

Unix Makefiles

After determining and waiting for the configuration to complete, a number of red items appear, representing options in the current project that can be configured by the user. Let’s check the USE_MYMATH option, as shown below:

Click Configure again until the red option is gone, and click Generate to Generate a user-configured build system for the project, as shown in the figure below:

After the build system is generated, observe how the projectconfig.h and main.cpp files change, and then proceed with the project build, using the same method as before, such as cmake –build mybuild.

If you don’t want to use the graphical tool, you can also pass the value of the variable directly to the cmake tool on the command line. For example:

cmake -B mybuild -S . -D USE_MYMATH:BOOL=ON
Copy the code

7. Install

The so-called installation can be simply understood as copying several files required by the software or program to a specified location. So what files do you need to install for our CMakeLearnDemo project? For MyMath, you need to install the library and header files; For the entire application, you need to install the executable and the projectconfig.h header.

After the installation rules are determined, we add the following at the end of myMath/cMakelists.txt:

install(TARGETS mymath DESTINATION lib)
install(FILES mymath.h DESTINATION include)
Copy the code

At the end of the root cmakelists.txt add:

install(TARGETS CMakeLearnDemo DESTINATION bin)
install(FILES autoGeneratedHeaders/projectConfig.h DESTINATION include)
Copy the code

The install command is used to generate installation rules for a project, indicating what needs to be installed. TARGETS are installation build TARGETS; FILES is the installation file. If the relative path is used, it is relative to the current source directory that cmake processes. DESTINATION

is the installation path, relative to the value of the CMAKE_INSTALL_PREFIX variable if a relative path is used.

Next, let’s open cmake-gui and configure it the same way as before, except this time we need to set the CMAKE_INSTALL_PREFIX variable, which is the installation prefix path of the project, as shown in the figure:

After configuring the project, generating the project build system, and building the project, you are ready to install with the following command:

cmake --install mybuild
Copy the code

When you are done, open the installation prefix path you set before, and you will already have the corresponding file, as shown in the figure below:

Test 8.

Now let’s test the CMakeLearnDemo application. At the end of the root cMakelists.txt, we can enable tests and then add some basic tests to verify that the application is working properly, as follows:

enable_testing(a)function(do_test target arg result)
    add_test(NAME sqrt${arg} COMMAND ${target} ${arg})
    set_tests_properties(sqrt${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()

do_test(CMakeLearnDemo 4 "2")
do_test(CMakeLearnDemo 2 "1.414")
do_test(CMakeLearnDemo 5 "2.236")
do_test(CMakeLearnDemo 123.456 "11.111")
do_test(CMakeLearnDemo -4 "NaN|NULL|Null|null|[Ee]rror|[Nn]ot [Ee]xist|[Nn]egative")
Copy the code

Explanation:

function(<name> [arg1 arg2 ...] ) do sth ...endfunction(a)Copy the code

As the name implies, to define a function, you must have a function name and optional arguments.

The add_test COMMAND adds a test, NAME < NAME > specifies the NAME of the test, and COMMAND < COMMAND > [arg…] Specifies the command line to be invoked at test time, and if is an executable target created by add_execuable(), it will automatically be replaced with the path to the executable generated at build time.

The set_tests_properties command sets properties for the specified test. The PASS_REGULAR_EXPRESSION property means that in order to pass the test, the output of the command must match this regular expression, such as “\d+(\.\d+)?” If the output is “result is 1.5”, the test passes. To learn more about what Properties Tests in Cmake have, read Properties on Tests.

After the project is built, use CD mybuild to jump to the build directory and type ctest -n to see the tests that will be run, but not actually run them, as shown below:

Next, run ctest -vv to run the test and output the detailed test information, as shown in the figure below:

9. Add a system self-check

Now, let’s add some code to the myMath :: SQRT function that depends on functionality that may not be supported on some target platforms, so we need to check. The code we want to add is to calculate the square root using the following mathematical formula, using the logarithm function and the exp exponential function. If the target platform does not support these two functions, we will use the previous method:

Now let’s add the following to myMath/cMakelists.txt:

include(CheckCXXSymbolExists)
check_cxx_symbol_exists(log "cmath" HAVE_LOG)
check_cxx_symbol_exists(exp "cmath" HAVE_EXP)

if(HAVE_LOG AND HAVE_EXP)
    target_compile_definitions(mymath
            PRIVATE "HAVE_LOG" "HAVE_EXP")
endif(a)Copy the code

Explanation:

There are many optional modules in cmake, and you can enable specific modules in a project by using the include command.

check_symbol_exists(<symbol> <files> <variable>)
Copy the code

The CheckCXXSymbolExists command checks whether the

symbol is available in the specified files C++ header file. The symbol can be defined as a macro, variable, or function name. Is unrecognizable; The result value of the check is placed in

.

The target_compile_definitions command is used to add a compilation definition to the target, which is the -d option in GCC:

g++ -D HAVE_LOG -D HAVE_EXP -o mymath.o -c mymath.cpp
Copy the code

This is equivalent to manually defining two macros in mymath.cpp:

#define HAVE_LOG
#define HAVE_EXP
Copy the code

To recap, the new addition checks to see if the log and exp functions are defined and available in the cMath header. If both are, add the HAVE_LOG and HAVE_EXP compiler definitions to myMath.

You can also use the configure_file() approach, but it’s more cumbersome.

Let’s change myMath :: SQRT to look like this:

#include "mymath.h"

# if defined(HAVE_LOG) && defined(HAVE_EXP)
#include <cmath>
#endif

namespace mymath
{
    double sqrt(double number)
    {
        static constexpr double precision = 1e-6;
        static constexpr auto abs= [] (double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

# if defined(HAVE_LOG) && defined(HAVE_EXP)
        return std: :exp(0.5 * std: :log(number) );
#endif

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        returnguess; }}Copy the code

– reload cMakelists. TXT in CLion, not surprising that the 2 lines that were included in the conditional compilation should now be highlighted, meaning that the log and exp functions are available on the current platform; Run the rebuild and see if it works.

10. Add custom commands and build files

Suppose for the purposes of this tutorial, we decide to move away from the log and exp functions and instead want to generate a table of precomputed values that can be used in the myMath :: SQRT function. In this section, we will create the tables during the build process and then compile them into the MyMath library.

In myMath/cMakelists.txt and myMath /mysqrt.cpp, let’s remove all the additions from the previous section. Then, in myMath /mysqrt.cpp, let’s add a new makeSqrtTable source file to generate the precalculated values table header file, as follows:

#include <iostream>
#include <fstream>
#include <cmath>

int main(int argc, char* argv[])
{
    // argv[1] : output header file path
    // argv[2] (optional) : max value of precomputed square root

    if(argc < 2)
    {
        std: :cerr << "Must have at least 2 command line arguments." << std: :endl;
        return 1;
    }

    int maxPrecomputedSqrtValue = 100;
    if(argc >= 3)
    {
        try
        {
            maxPrecomputedSqrtValue = std::stoi(argv[2]);
        }
        catch(const std::invalid_argument& e)
        {
            std: :cerr << e.what() << std: :endl;
            return 1; }}std: :ofstream ofstrm(argv[1].std::ios_base::out | std::ios_base::trunc);
    if(! ofstrm.is_open()) {std: :cerr << "Can not open " << argv[1] < <" to write!" << std: :endl;
        return 1;
    }

    ofstrm << "namespace mymath\n{\n\tstatic constexpr int maxPrecomputedSqrtValue = " << maxPrecomputedSqrtValue << "; \n";
    ofstrm << "\tstatic constexpr double sqrtTable[] =\n\t{\n";

    for(int i = 0; ; i++)
    {
        double precomputedSqrtValue = std: :sqrt(i);
        ofstrm << "\t\t" << precomputedSqrtValue;
        if(i == maxPrecomputedSqrtValue)
        {
            ofstrm << "\n\t}; \n}";
            break;
        }
        else
        {
            ofstrm << ",\n";
        }
    }

    ofstrm.close();

    return 0;
}
Copy the code

Mymath/cMakelists.txt:

add_executable(MakeSqrtTable makeSqrtTable.cpp)

add_custom_command(
        OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h
        COMMAND MakeSqrtTable ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h 1000
        DEPENDS MakeSqrtTable
)

add_library(mymath STATIC mymath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h )
Copy the code

Explanation:

The first line goes without saying. The add_custom_command command that follows is used to add custom build rules to the build system generation in a variety of ways, but in this case it is used to define the commands used to generate the specified output file. OUTPUT output1 […] Specifies which output files are available; COMMAND commands [args …] In this case, we call MakeSqrtTable and pass two parameters, one is the output file path and the other is the maximum precomputed square root value. Since MakeSqrtTable is a build target, a target level dependency is automatically created. To ensure that the target is built before this command is invoked; DEPENDS [depend …] Declare the file on which the command is to be executed, and it will also create a target-level dependency when the dependency is a build target, and if the build target is an executable file or library, it will also create a file-level dependency to re-run the custom command when the build target is recompiled.

Since the OUTPUT file path is passed as an argument in COMMAND, OUTPUT output1 [… What does the option do? In fact, custom commands are invoked in the build target during the build process. Which build target? In the same cMakelists. TXT, specify any OUTPUT file declared in the OUTPUT option of add_custom_command as the build target of the source file, in this case the myMath library. If there is no build target that uses the OUTPUT file, then the custom command will not be called. Although the header file is included in CPP, it does not need to be displayed in the add build target command, but in order for the custom command to be invoked, it does. Also, do not list output files in more than one separate target that may be built in parallel, or the resulting output instances may conflict.

Finally, modify the myMath :: SQRT function to look like this:

#include "mymath.h"
#include "sqrtTable.h"
#include <iostream>

namespace mymath
{
    double sqrt(double number)
    {
        static constexpr double precision = 1e-6;
        static constexpr auto abs= [] (double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

        int integerPart = static_cast<int>(number);
        if(integerPart <= maxPrecomputedSqrtValue && abs(number - integerPart) <= precision)
        {
            std: :cout << "use precomputed square root : " << integerPart << std: :endl;
            return sqrtTable[integerPart];
        }

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        returnguess; }}Copy the code

Rebuild and run myMath :: SQRT to see if it uses a precomputed table. An example of the results is shown below:

11. Package projects

This is the final step of the tutorial. What we will do is to use the Cpack tool to package the project. There are two forms of package: source code package and binary installation package:

A source code package is a package of source code for a version of a piece of software so that, once distributed, users who download it can configure, build, and install it as they see fit. The software package can be tar.gz,. Zip, or.7z.

A binary installation package is when the author prebuilds a version of the software and packages the installation file (specified by the install() command) into a package for the user to install. The software package can be a simple tar.gz package, a. Shshell script, a. Deb debian installation package, or a Windows installation package.

Now let’s briefly describe the workflow of using CPACK packaging in a project:

  • For each installer or package format, CPACK has a specific back-end handler, called a “generator,” that generates the required installation package and invokes the specific package creation tool.

  • We can set the value of the associated cmake variable in cMakelists.txt to control the various properties of the generated package, known as “customization”. All software packages have common properties, such as CPACK_PACKAGE_NAME, CPACK_PACKAGE_VERSION, and so on. Of course, each software package has its own properties that can be set. After setting the properties, include the CPACK module:

    include(CPack)
    Copy the code
  • During the process of generating the project build system, cmake generates two configuration files in the build directory based on the properties we set above: Cmake and CPackSourceConfig. Cmake, one for controlling binary installation package generation and one for controlling source code package generation.

  • After the project build system is generated, you can use the CPack command line tool to generate the source code package in the build directory. Once the project is built, the binary installation package can be generated. The default is to generate a binary installation package. If you want to generate a source code package, you need to specify, such as:

    cpack --config CPackSourceConfig.cmake -G tar.gz
    Copy the code

There are many other options you can set for cpack, see Cpack Options for details.

This project is just a tutorial, but since you want to generate software packages for users to use, or add a software LICENSE description file is more formal ha, in the root of the project to create a new LICENSE file, the content is as follows:

Copyright (c) 2019: Siwei Zhu

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copy the code

Setting the various properties of the package can take up a lot of lines, which can add a lot to the contents of the cMakelists.txt file, which would be easier for project configuration management if they were placed in a separate file. In fact, the cmake code can be placed in a file with the.cmake extension as well as in cMakelists.txt. The include() command can include either a cmake module or a.cmake file. It is similar to #include in c++, which includes the cmake code from other files. Each cmake module has a

.cmake file. So including modules is essentially the same as including.cmake files. Next let’s create a projectCpack. cmake file in the root of the project to configure the installation and packaging of the project, and then remove the lines of install(XXX) from the root cMakelists.txt file. And replace it with include(projectcpack.cmake). Finally, projectCpack.cmake looks like this:

# Install content
install(TARGETS CMakeLearnDemo DESTINATION bin)
install(FILES autoGeneratedHeaders/projectConfig.h DESTINATION include)
install(FILES LICENSE DESTINATION .)

Set the name of the package
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
Set the provider of the package
set(CPACK_PACKAGE_VENDOR "siwei Zhu")
Set the description of the package
set(CPACK_PACKAGE_DESCRIPTION "a simple cmake learn demo.")
Set the LICENSE
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
Set package version information
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
Set the name of the package file to be generated, excluding the extension
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_SYSTEM_NAME}")
Set the source code package to ignore files, similar to gitignore
set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR}; /cmake-build-debug/; /.git/; .gitignore")
Set the list of source package generators
set(CPACK_SOURCE_GENERATOR "ZIP; TGZ")
Set the list of binary package generators
set(CPACK_GENERATOR "ZIP; TGZ")
Set the package installation prefix directory
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/${PROJECT_NAME}")

if(UNIX AND CMAKE_SYSTEM_NAME MATCHES "Linux")
    # Add generator for deb installation package
    list(APPEND CPACK_GENERATOR "DEB")
    # Package maintainer
    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "siwei Zhu")
    Devel refers to the development tool class software
    set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
    # package depends on
    set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6")
elseif(WIN32 OR MINGW)
    # Add Windows NSIS installation package generator
    list(APPEND CPACK_GENERATOR "NSIS")
    Set the package installation directory
    set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}")
    The default installation directory provided by the NSIS installer to the end user is located in this root directory.
    ${CPACK_NSIS_INSTALL_ROOT}/${CPACK_PACKAGE_INSTALL_DIRECTORY}
    set(CPACK_NSIS_INSTALL_ROOT "C:\\Program Files\\")
    Set contact information for questions and comments about the installation process
    set(CPACK_NSIS_CONTACT "siwei Zhu")
    # First ask to uninstall the previous version. If I set it to ON,
    # Then setup will look for the previously installed version and, if found, ask the user if you want to uninstall it before proceeding with the installation.
    set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
endif(a)include(CPack)
Copy the code

In the above code, we first set the file to install, several properties of the package, and finally the CPack module. Zip,.tar.gz, and.deb for Linux, and NSIS for Windows or MINGW.

Rebuild the project build system and build. When finished, go to the mybuild directory and run the following 2 commands:

cpack
cpack --config CPackSourceConfig.cmake
Copy the code

To generate source and binary packages (all package types specified by CPACK_GENERATOR or CPACK_SOURCE_GENERATOR variables if the -g option is not used).

Open the myBuild directory, and you should be able to generate several packages, as shown below:

We double-click the cMakelearndemo-1.0.0-source.tar. gz file and use the archive manager to view the directory structure of the Source code package as follows:

Let’s double-click on cMakelearndemo-1.0.0 -.deb to install our binary package, as shown in the figure below:

When the installation is complete, go to the /opt directory and you can see that the installation file is already there:

Open the terminal and type:

sudo dpkg -l | grep cmakelearndemo
Copy the code

The results are as follows:

You can see that the installation record of this package is available. Finally, type:

sudo apt remove cmakelearndemo
Copy the code

To uninstall the software package, as shown below:

CMake learning experience and resource sharing

Finally, here are some ways to learn CMake and share resources:

First of all, the tutorial above should have covered most of the common cmake commands and methods. The official cMake tutorial has a total of more than a dozen steps, but this article only includes the first seven. The reason is that the official cmake documentation is not well written, not to say that the content is not detailed, but mainly the lack of use cases. And some places are illogical, especially in this cmake tutorial, the first few steps look more smooth, at one go, the more you see the back of the more confusing, some places added a file, the result is not a word, the document content is not given, is really suffocating; Another reason is that some of the features of the latter steps are used less and are not suitable for the introduction tutorial. If I have time, I can use them to write a separate article.

For example, if I want to see which cmake variables can be used in project configuration, select cmake variables, as shown in the figure below:

To be more specific, if you want to search for details about the use and function of a particular command or variable, you can enter the search in the search box on the left, as shown below:

Finally, I stumbled upon the folk Chinese translation of the CMake Cookbook, in gitbook form, directly online, very convenient. Books on cmake are rare, let alone in Chinese, so cherish them.

Getting Started Project source code repository

Code – CMakeLearnDemo clouds