[Из песочницы] Использование Docker для сборки и запуска проекта на C++

habr.png

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

#include 

// Наше приложение будет иметь один параметр командной строки - "--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 

#include 

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

© Habrahabr.ru