Lab 1 The Software build process

Aims

The aim of this lab is to introduce the C++ software build process, this will include.

  1. Single file software build
  2. Using external libraries and package managers (vcpkg)
  3. Build Tools (make,cmake)

Getting Started

We will start this project in a new folder

From here we are going to use the touch to create an empty file and open it in Visual Studio Code. At this point it may be worth reading up on the use of C++ in VS Code from here and installing the C/C++ extensions

We are now going to create a simple “hello world” program by adding the following code to the file hello.cpp

#include <iostream>
#include <cstdlib>

int main()
{
  std::cout<<"hello world \n";
  return EXIT_SUCCESS;
}

Remember to save the file before we compile it (CTRL + s) then we can compile and run the program as follows.

g++ -Wall -g -std=c++17 hello.cpp -o hello

Compiler flags

In the previous example we used the following flags

flaguse
-Wallenable all warnings
-genable debug
-o [name]output to executable [name]

Package Management

In our current lab build we have a number of C++ libraries installed using the vcpkg package manager.

This tool allows us to install and build libraries and include them using a number of build tools on a number of platforms.

You can install your own version at home by following the instructions here, once installed we can install the fmt library we are going to use in this example by changing to the vcpkg directory and running

./vcpkg install fmt

We are going to create another empty file called fmt.cpp

and add the following code which is using the fmt:: library, which contains an implementation of the new c++ 20 format library amongst other things.

#include <fmt/format.h>
#include <cstdlib>
int main()
{
  fmt::print("This is using placeholders {} {} {} \n",1,2,3);
  return EXIT_SUCCESS;
}

If we attempt to compile this as we have done with the previous examples we get the following.

g++ -Wall -g -std=c++17 fmt.cpp -o fmt

The reason we get this error is because we have not told g++ where to find the header files. By default g++ uses the following (in the University linux labs1).

gcc -xc++ -E -v -

/public/devel/2020/include /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/../../../../include/c++/7
/opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/../../../../include/c++/7/x86_64-redhat-linux
/opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/../../../../include/c++/7/backward
/opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/include
/usr/local/include
/opt/rh/devtoolset-7/root/usr/include
/usr/include

As the fmt library was installed using vcpkg we need to tell the compiler where to search. In the case of the university it is the following and we use the -I flag to tell the compiler where to search for include files.

g++ -Wall -g -std=c++17 -I /public/devel/2021/vcpkg/installed/x64-linux/include/ fmt.cpp -o fmt

You will now notice that whilst the error with the inclusion of <fmt/format.h> has gone we now get a new error from not the compiler but the linker (ld). In this case the error is an “undefined reference to” error which usually signifies that the linker has looked for a function defined in a header file but but can’t find it. We now have to tell the compiler / linker where to find the libraries and which ones to add (link). This is done as follows

g++ -Wall -g -std=c++17 -I//public/devel/2021/vcpkg/installed/x64-linux/include fmt.cpp -L /public/devel/2021/vcpkg/installed/x64-linux/lib -lfmt

In the previous example we used the following flags

flaguse
-I [path]add include search path
-L [path]add library search path
-l [name]add library

Libraries

Note the naming convention for unix libraries are as follows :-

graph LR;
  l1(lib)-->l2(Library Name)-->l3(".so | .a")

A .so file is a dynamic library and a .a file is a static library. By default vcpkg builds static libraries under linux. When adding them to the linker with the -l flag we omit the lib and the .a .so extensions. By default the linker will use a .so file if found else will search for the .a version. We will look at the implications for this in a future session.

Header Only Libraries

C++ allows use to develop “header only” libraries as well as compiled dynamic and static ones. These do have some advantages as it can make our compilation process easier (we don’t need the -L / -l flags) and as the libraries are added to the compilation process can avoid ABI issues. The disadvantage is the possible increase in compilation time.

The fmt library has the ability to be included as a header only version as follows.

g++ -Wall -g -std=c++17 -DFMT_HEADER_ONLY -I/public/devel/2020/vcpkg/installed/x64-linux/include fmt.cpp -o fmt

In the case the -D flag adds a definition to the compiler command line which is the equivalent of using the #define pre-processor macro in C++ so in this case it is like using in the source file.

#define FMT_HEADER_ONLY

Automating the build

As you can see from the above examples there is a lot of typing to repeat each time we wish to build a new project. To help overcome this we can use the make tool.

Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program.

We will add the following lines to the Makefile

CFLAGS = -Wall -g -std=c++17
DEFINES = -DFMT_HEADER_ONLY
INCLUDE_PATH = -I /public/devel/2020/vcpkg/installed/x64-linux/include
OBJECTS=fmt.o 

fmt : $(OBJECTS)
	g++  $(OBJECTS) -o fmt

fmt.o : fmt.cpp
	g++ -c $(CFLAGS) $(INCLUDE_PATH) $(DEFINES) fmt.cpp

clean :
	rm -f *.o fmt

The makefile itself is a list of defines ( CFLAGS = -Wall) and a basic “recipe” for how to build the elements. For example the line

fmt : $(OBJECTS)
	g++  $(OBJECTS) -o fmt

Says the file fmt is made from the defines in the variable OBJECT to to build that we need to execute g++ $(OBJECTS) -o fmt

Make is very sensitive to spaces and tabs. In the above example the command lines (the ones below the recipe) are indented with a tab. For more information see here

By default will look for a file called makefile or Makefile however in this case we have named it Makefile.linux so we have to use the following commands to use it.

There is another target added to this makefile called clean which will remove and of the build files and the executable, this is a common practice when using make.

Using cmake

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice.

One of the advantages of tools like cmake is that it uses a “metalanguage” to create a platform specific makefile (or under windows MSBuild / Visual Studio project). The following examples are based on the book Professional CMake (A Practical Guide) By Craig Scott where he describes CMake thus

Without a build system, a project is just a collection of files. CMake brings some order to this, starting with a human-readable file called CMakeLists.txt that defines what should be built and how, what tests to run and what package(s) to create. This file is a platform independent description of the whole project, which CMake then turns into platform specific build tool project files. As its name suggests, it is just an ordinary text file which developers edit in their favorite text editor or development environment.

First we will create a CMakeLists.txt file in the root of the project directory.

# We will always try to use a version > 3.1 if avaliable
cmake_minimum_required(VERSION 3.2)
# name of the project It is best to use something different from the exe name
project(fmt_build) 

# Here we set the C++ standard to use
set(CMAKE_CXX_STANDARD 17)

# Now we are going to search for our fmt library this will be a .cmake file
# in this case with a name like  fmtConfig.cmake or fmt-config.cmake
find_package(fmt CONFIG REQUIRED)

# Now we add our target executable and the file it is built from.
add_executable(fmt fmt.cpp)

# Finally we need to link our libraries. In this case we specify we want to use 
# The header only version of fmt which will set the flags on the compile line.

target_link_libraries(fmt PRIVATE  fmt::fmt-header-only)

We can now build the program using the following.

The define -DCMAKE_TOOLCHAIN_FILE=/public/devel/2021/vcpkg/scripts/buildsystems/vcpkg.cmake tells cmake where to look for build information for the current system. In this case vcpkg provides these for us. Typing this each time can become tiresome so it is possible to set this as an environment variable in the .bash_profile file.

export CMAKE_TOOLCHAIN_FILE=/public/devel/2021/vcpkg/scripts/buildsystems/vcpkg.cmake

And modify our cmake file to check for this environment variable.

# We will always try to use a version > 3.1 if avaliable
cmake_minimum_required(VERSION 3.2)

if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{CMAKE_TOOLCHAIN_FILE})
   set(CMAKE_TOOLCHAIN_FILE $ENV{CMAKE_TOOLCHAIN_FILE})
endif()


# name of the project It is best to use something different from the exe name
project(fmt_build) 

# Here we set the C++ standard to use
set(CMAKE_CXX_STANDARD 17)

# Now we are going to search for our fmt library this will be a .cmake file
# in this case with a name like  fmtConfig.cmake or fmt-config.cmake
find_package(fmt CONFIG REQUIRED)

# Now we add our target executable and the file it is built from.
add_executable(fmt fmt.cpp)

# Finally we need to link our libraries. In this case we specify we want to use 
# The header only version of fmt which will set the flags on the compile line.

target_link_libraries(fmt PRIVATE  fmt::fmt-header-only)

Some Useful examples

The following example shows some usage for cmake / make that may be useful


  1. You will notice that ‘/public/devel/2021/include’ is part of this and this has been setup by me as part of the University build by setting the CPATH environment variable. ↩︎

Next