Lab 1 The Software build process
Aims
The aim of this lab is to introduce the C++ software build process, this will include.
- Single file software build
- Using external libraries and package managers (vcpkg)
- 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
flag | use |
---|---|
-Wall | enable all warnings |
-g | enable 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
flag | use |
---|---|
-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
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