C++ Compiler und Docker

  • Deutsch
  • English

Docker ist hervorragend dafür geeignet, in einer Toolchain eine Komponente, wie beispielsweise einen Compiler dynamisch auszutauschen. Das ermöglicht es, eine Umgebung aufzubauen, in der beispielsweise ein Compiler mit relativ wenig Aufwand ausgetauscht werden kann. Das kann dafür genutzt werden, um eine Software mit unterschiedlichen Compilern, in verschiedenen Versionen zu kompilieren. 

In den folgenden Schritten werden Docker Container für unterschiedliche Compiler und Versionen erstellt. Diese Container werden dann dazu verwendet, eine C++ Applikation mit verschiedenen Konfigurationen und C++ Standards zu kompilieren. Die Compiler generieren jeweils ein Artefakt und dabei wird der Output des Compilers gesichert. So kann man die Warnung und Hinweise der Compiler aggregieren und später in einer Continuous Integration automatisiert auswerten.

Außerdem bietet das Aufsetzen der Compiler in Docker Containern den Vorteil, dass diese auch auf verschiedenen Betriebssystemen verwendet werden können. Beispielsweise ist der Standardcompiler auf dem Macbook der clang. Mit den Containern ist es aber einfach möglich, seinen Code nicht nur mit clang, sondern auch mit dem GCC Compiler zu übersetzen.

Der Artikel zeigt wie die Dockerfiles zum Erstellen der Images aufgebaut sind und wie die Container gebaut werden. Die Container sind über eine docker-compose Datei verbunden und werden nacheinander gestartet, um in diesem Verbund aus Compilern, die Beispielapplikation zu übersetzen.

Die Projektstruktur

Das Projekt kann auf Github geklont werden. Es befindet sich ein Dockerfile für jede Compilerversion in dem Projektordner. Die Zentrale Datei ist die docker-compose.yaml. Die Compose-Datei bildet den Verbund und sorgt dafür, dass die Container alle erstellt und gestartet werden:

cpp-compiler/
├── clang
│   └── clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz
├── docker-compose.yaml
├── Dockerfile-4.9
├── Dockerfile-8.4
├── Dockerfile-9.3
├── Dockerfile-clang10
├── entrypoint.sh
├── Jenkinsfile
└── src
    ├── main.cpp
    └── Makefile

mkdir ~/docker-cpp && cd ~/docker-cpp
mkdir src
mkdir clang-10 && cd clang-10 && wget https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz
cd ~/docker-cpp 

C++ Beispielapplikation

Zum Testen der Container und zu überprüfen ob die Compiler richtig arbeiten, wird eine minimale C++ Applikation benutzt. Diese ist unter src/main.cpp abgespeichert und kann so von allen Compilern übersetzt werden:

#include <iostream>
#include <string>

int& func(double const * f)
{
	int x = 0x2;
	std::cout << std::to_string(*f) << '\n';
	return x;
}

void func2(const std::string& s)
{
  std::cout << s << std::endl;
}

int main(){
  int x = 0;
  float f = 2.4f;
  double d = 21.2;

  x += f;
  func(&d);

  std::string s("test");

  func2(s);

  std::cout << "int : " << x << "\n";
  std::cout << "float : " << f << "\n";

  return 0;
}

GCC - Die Gnu Compiler Collection

Das erste Dockerfile erstellt einen Container mit einem GCC C++ Compiler in der Version 4.9. Dafür greifen wird ein vorhandenens Image von Docker Hub verwendet. Das spart Zeit und Installationsaufwand und lässt sich leicht austauschen. Das Image wird von Docker runtergeladen und erstellt einen Container, der dann direkt zur Verfügung steht:

FROM gcc:4.9

WORKDIR /code
COPY src/* /code/

ENV CXX=g++
RUN gcc --version
ENTRYPOINT [ "make" ]

Die Datei definiert, wie der Container auf Basis dieses Images erstellt und mit welchen Befehlen der Container ausgeführt werden soll. Es wird festgelegt, welches Images also Grundlage für den Container verwendet werden soll (hier gcc-4.9) und ein Arbeitsverzeichnis angegeben, in dem der Conainer sich nach dem Start befindet(hier: /code). Von dem Hostsystem wird der Inhalt des /src Ordners in den /code in dem Container kopiert und mit der Umgebungsvariable CXX wird der Compiler festgelegt. 

Die Dockerdatei wird für das Erstellen eines Container, mit einer bestimmten Version benutzt. Damit also mehrere Compiler in verschiedenen Containern verfügbar sind, legt man weitere Dockerfiles an. In diesem Beispiel werden für  die zwei weiteren GCC Compilerversionen also zwei weitere Dockerfiles erstellt, die das entsprechende GCC-Image von Docker Hub definieren:

FROM gcc:8.4

WORKDIR /code
COPY src/* /code/

ENV CXX=g++
RUN gcc --version
ENTRYPOINT [ "make" ]
FROM gcc:9.3

WORKDIR /code
COPY src/* /code/

ENV CXX=g++
RUN gcc --version
ENTRYPOINT [ "make" ]

Insgesamt sind so drei Dockerfiles entstanden, die jeweils einen GCC Compiler bereitstellen.