Using Docker to build and run a project in C ++
This post will discuss how to build a C ++ project using GTest and Boost using Docker. The article is a recipe with some explanatory comments. The solution presented in the article does not claim to be Production-ready.
Why and who might need it?
Suppose that, like me, I really like the concept of Python venv , when all the necessary dependencies are located in a separate, strictly defined directory; or you need to ensure simple portability of the build and test environment for the project being developed, which is very convenient, for example, when a new developer joins the team.
This article will be especially useful for novice developers who need to perform basic environment settings for building and running a C ++ project.
The environment presented in the article can be used as a framework for test tasks or laboratory work.
Docker installation
All that you need for the project presented in this article is Docker and Internet access.
Docker is available for Windows, Linux and Mac platforms. Official documentation .
Since I use a Windows machine on board, it was enough for me to download the installer and run it.
It should be noted that at the moment Docker for Windows uses Hyper-V for its work.
Project
As a project, we mean the CommandLine application, which displays the string "Hello World!" to standard output stream.
The project will use the required minimum of libraries, as well as CMake as an assembly system.
The structure of our project will be as follows:
project
| Dockerfile|
\---src
CMakeLists.txt
main.cpp
sample_hello_world.h
test.cppThe CMakeLists.txt file contains the project description.
Source file:
cmake_minimum_required(VERSION 3.2)
project(HelloWorldProject)
# используем C++17set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
# используем Boost.Program_options# дабы не переусложнять, в качестве статической библиотекиset(Boost_USE_STATIC_LIBS ON)
find_package(Boost COMPONENTS program_options REQUIRED)
include_directories(${BOOST_INCLUDE_DIRS})
# исполняемый файл нашего приложенияadd_executable(hello_world_app main.cpp sample_hello_world.h)
target_link_libraries(hello_world_app ${Boost_LIBRARIES})
# включаем CTestenable_testing()
# в качестве фреймворка для тестирования используем GoogleTestfind_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
# исполняемый файл тестовadd_executable(hello_world_test test.cpp sample_hello_world.h)
target_link_libraries(hello_world_test ${GTEST_LIBRARIES} pthread)
# добавим этот файл в тестовый набор CTestadd_test(NAME HelloWorldTest COMMAND hello_world_test)The file sample_hello_world.h contains the HelloWorld class description, sending an instance of which to the stream will display the string "Hello World!". This complexity is due to the need to test the code of our application.
Source file:
#ifndef SAMPLE_HELLO_WORLD_H#define SAMPLE_HELLO_WORLD_Hnamespace sample {
structHelloWorld {template<classOS>
friendOS& operator<<(OS& os, const HelloWorld&) {
os << "Hello World!";
return os;
}
};
} // sample
#endif // SAMPLE_HELLO_WORLD_HThe main.cpp file contains the entry point of our application, we also add Boost.Program_options to simulate a real project.
Source file:
#include"sample_hello_world.h"#include<boost/program_options.hpp>#include<iostream>// Наше приложение будет иметь один параметр командной строки - "--help"autoparseArgs(int argc, char* argv[]){
namespace po = boost::program_options;
po::options_description desc("Allowed options");
desc.add_options()
("help,h", "Produce this message");
auto parsed = po::command_line_parser(argc, argv)
.options(desc)
.allow_unregistered()
.run();
po::variables_map vm;
po::store(parsed, vm);
po::notify(vm);
// В C++17 больше нет необходимости использовать std::make_pairreturnstd::pair(vm, desc);
}
intmain(int argc, char* argv[])try{
auto [vm, desc] = parseArgs(argc, argv);
if (vm.count("help")) {
std::cout << desc << std::endl;
return0;
}
std::cout << sample::HelloWorld{} << std::endl;
return0;
} catch (std::exception& e) {
std::cerr << "Unhandled exception: " << e.what() << std::endl;
return-1;
}The test.cpp file contains the required minimum — a test of the HelloWorld class functionality. For testing, use GoogleTest .
Source file:
#include"sample_hello_world.h"#include<sstream>#include<gtest/gtest.h>// Простой тест, выводим HelloWorld в поток, сравниваем вывод с ожидаемым
TEST(HelloWorld, Test) {
std::ostringstream oss;
oss << sample::HelloWorld{};
ASSERT_EQ("Hello World!", oss.str());
}
// Точка входа в тестовое приложениеintmain(int argc, char **argv){
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}Next, we turn to the most interesting part - setting up the build environment with the help of Dockerfile!
Dockerfile
For the assembly we will use the latest version of gcc .
Dockerfile contains two stages: building and running our application.
To start using the latest version of Ubuntu.
Contents of Dockerfile:
# Сборка ---------------------------------------# В качестве базового образа для сборки используем gcc:latest
FROM gcc:latest as build
# Установим рабочую директорию для сборки GoogleTest
WORKDIR /gtest_build
# Скачаем все необходимые пакеты и выполним сборку GoogleTest# Такая длинная команда обусловлена тем, что# Docker на каждый RUN порождает отдельный слой,# Влекущий за собой, в данном случае, ненужный оверхед
RUN apt-get update && \
apt-get install -y \
libboost-dev libboost-program-options-dev \
libgtest-dev \
cmake \
&& \
cmake -DCMAKE_BUILD_TYPE=Release /usr/src/gtest && \
cmake --build . && \
mv lib*.a /usr/lib
# Скопируем директорию /src в контейнер
ADD ./src /app/src
# Установим рабочую директорию для сборки проекта
WORKDIR /app/build
# Выполним сборку нашего проекта, а также его тестирование
RUN cmake ../src && \
cmake --build . && \
CTEST_OUTPUT_ON_FAILURE=TRUE cmake --build . --target test
# Запуск ---------------------------------------# В качестве базового образа используем ubuntu:latest
FROM ubuntu:latest
# Добавим пользователя, потому как в Docker по умолчанию используется root# Запускать незнакомое приложение под root'ом неприлично :)
RUN groupadd -r sample && useradd -r -g sample sample
USER sample
# Установим рабочую директорию нашего приложения
WORKDIR /app
# Скопируем приложение со сборочного контейнера в рабочую директорию
COPY --from=build /app/build/hello_world_app .
# Установим точку входа
ENTRYPOINT ["./hello_world_app"]I suppose, for now, proceed to build and run the application!
Build and Run
To build our application and create a Docker image, it is enough to run the following command:
# Здесь docker-cpp-sample название нашего образа# . - подразумевает путь к директории, содержащей Dockerfile
docker build -t docker-cpp-sample .To run the application, use the command:
> docker run docker-cpp-sampleWe will see the cherished words:
Hello World!To pass a parameter, it is enough to add it to the above command:
> docker run docker-cpp-sample --help
Allowed options:
-h [ --help ] Produce this messageSumming up
As a result, we created a full-fledged C ++ application, set up the environment for building and running it under Linux, and wrapped it in a Docker container. Thus, freeing subsequent developers from having to spend time setting up a local assembly.