В этой публикации речь пойдет о том, как выполнить сборку 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-контейнер. Таким образом, освободив последующих разработчиков от необходимости тратить время на настройку локальной сборки.
