В этой публикации речь пойдет о том, как выполнить сборку C++ проекта, использующего GTest и Boost, при помощи Docker. Статья представляет собой рецепт с некоторыми поясняющими комментариями, представленное в статье решение не претендует на статус Production-ready.
Зачем и кому это может понадобиться?
Предположим, что вам, как и мне очень нравится концепция Python venv, когда все нужные зависимости расположены в отдельной, строго определенной директории; или же вам необходимо обеспечить простую переносимость среды сборки и тестирования для разрабатываемого проекта, что очень удобно, например, при присоединении нового разработчика к команде.
Эта статья будет особенно полезна начинающим разработчикам, кому необходимо выполнить базовую настройку окружения для сборки и запуска C++ проекта.
Представленное в статье окружение можно использовать как каркас для тестовых заданий или лабораторных работ.
Установка Docker
Все, что вам понадобится, для реализации проекта, представленного в этой статье — это Docker и доступ в интернет.
Docker доступен под платформы Windows, Linux и Mac. Официальная документация.
Так как я использую машину с Windows на борту, мне было достаточно скачать инсталятор и запустить.
Следует учесть, что на данный момент Docker под Windows использует Hyper-V для своей работы.
Проект
В качестве проекта будем подразумевать CommandLine приложение, выводящее строку "Hello World!" в стандартный поток вывода.
В проекте будет использован необходимый минимум библиотек, а также CMake в качестве системы сборки.
Структура нашего проекта будет выглядеть следующим образом:
project
| Dockerfile
|
\---src
CMakeLists.txt
main.cpp
sample_hello_world.h
test.cpp
Файл CMakeLists.txt содержит описание проекта.
Исходный код файла:
cmake_minimum_required(VERSION 3.2)
project(HelloWorldProject)
# используем C++17
set(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})
# включаем CTest
enable_testing()
# в качестве фреймворка для тестирования используем GoogleTest
find_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)
# добавим этот файл в тестовый набор CTest
add_test(NAME HelloWorldTest COMMAND hello_world_test)
Файл sample_hello_world.h содержит описание класса HelloWorld, отправляя экземпляр которого в поток, будет выводиться строка "Hello World!". Такая сложность обусловлена необходимостью тестирования кода нашего приложения.
Исходный код файла:
#ifndef SAMPLE_HELLO_WORLD_H
#define SAMPLE_HELLO_WORLD_H
namespace sample {
struct HelloWorld {
template<class OS>
friend OS& operator<<(OS& os, const HelloWorld&) {
os << "Hello World!";
return os;
}
};
} // sample
#endif // SAMPLE_HELLO_WORLD_H
Файл main.cpp содержит точку входа нашего приложения, добавим также Boost.Program_options, чтобы симулировать реальный проект.
Исходный код файла:
#include "sample_hello_world.h"
#include <boost/program_options.hpp>
#include <iostream>
// Наше приложение будет иметь один параметр командной строки - "--help"
auto parseArgs(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_pair
return std::pair(vm, desc);
}
int main(int argc, char* argv[]) try {
auto [vm, desc] = parseArgs(argc, argv);
if (vm.count("help")) {
std::cout << desc << std::endl;
return 0;
}
std::cout << sample::HelloWorld{} << std::endl;
return 0;
} catch (std::exception& e) {
std::cerr << "Unhandled exception: " << e.what() << std::endl;
return -1;
}
Файл test.cpp содержит необходимый минимум — тест функциональности класса HelloWorld. Для тестирования используем GoogleTest.
Исходный код файла:
#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());
}
// Точка входа в тестовое приложение
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Далее, переходим к самому интересному — настройке сборочного окружения при помощи Dockerfile!
Dockerfile
Для сборки будем использовать gcc последней версии.
Dockerfile содержит два этапа: сборка и запуск нашего приложения.
Для запуска используем Ubuntu последней версии.
Содержимое 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"]
Полагаю, пока переходить к сборке и запуску приложения!
Сборка и запуск
Для сборки нашего приложения и создания Docker-образа достаточно будет выполнить следующую команду:
# Здесь docker-cpp-sample название нашего образа
# . - подразумевает путь к директории, содержащей Dockerfile
docker build -t docker-cpp-sample .
Для запуска приложения используем команду:
> docker run docker-cpp-sample
Увидим заветные слова:
Hello World!
Для передачи параметра достаточно будет добавить его в вышеприведенную команду:
> docker run docker-cpp-sample --help
Allowed options:
-h [ --help ] Produce this message
Подводим итоги
В результате мы создали полноценное C++ приложение, настроили окружение для его сборки и запуска под Linux и завернули его в Docker-контейнер. Таким образом, освободив последующих разработчиков от необходимости тратить время на настройку локальной сборки.