Build Server Scripting


  1. Einleitung
    1. Ziele
  2. Windows Server Implementierung
    1. Docker Setup
    2. Source Build Script
    3. Server Build Script 4 Buildserver Produkte
  3. GitLab CI
    1. Gitlab Buildprozess
    2. Beispieldatei

Einleitung

Während früher ein Entwickler auf seiner eigenen Maschine nur zur Mond- oder Sonnenwende ein neues Software-Kompilat erzeugen wollte, spricht man heute von Continuous Integration und Continous Delivery (kurz CI und CD) und meint damit meist, dass zu jeder Zeit ein Softwarestand lieferfertig gemacht werden können soll.

Azure-DevOps oder Team-Foundation Server, TeamCity, Jenkins, GitLab … so heißen einige der bekannten Build-Server, mit denen man automatisiert das Kompilieren von Quellcodes anstoßen kann. Sie alle bringen ihre Vorteile am Einsatzort zum Tragen und helfen dabei, die Auslieferung von Software zu automatisieren und auch zu standardisieren.

Ich möchte aber für meine Sourcen auf meinen minimalistischen Systemen keine aufwändigen Dienste wie Datenbanken und Webserver installieren, und suche daher eine Möglichkeit, Builds ohne Zusatztools automatisch anzustarten.

Ziele

  • Ein Docker Image stellt eine Build-Umgebung mit allen nötigen Tools bereit
  • Ein Script lädt Quellcodes per GIT am Host-System herunter.
  • Der Docker Container erhält Zugriff auf die Sourcen am Host und startet ein individuelles Build-Script
  • Ein Script sammelt die Build-Ergebnisse ein und legt sie komprimiert in einem Unterverzeichnis ab.
graph LR S[Start
Server
Script] subgraph Shared Folder between Host and Docker G(GIT
Download) SS((Source
Build
Script)) D(Docker
Container) A(Store
Artifacts) end F[Finish
Server
Script] S --> G G --> D D --> A A --> F D --> SS SS --> D

Windows Server Implementierung

Voraussetzungen

Die Build-Automations-Scripts können auf einem Windows Server mit Docker Unterstützung mitlaufen. Das Beispiel sollte allerdings auch mit Docker Desktop funktionieren.

Wir brauchen dafür eine Docker-kompatible Visual Studio Umgebung für C++ Projekte. Natürlich sind auch andere Umgebungen möglich, auf diese wird aber hier nicht eingegangen.

Dann muss GIT für Windows auf dem Server installiert werden.

Und am Ende benötigen wir einen Ordner, in dem die Projekte heruntergeladen und gebaut werden können.

Docker Setup

Ab Visual Studio 2017 ist das Bauen in Docker offiziell unterstützt. Die Installation wird vom VS-Build-Tools Installer eingeleitet, der unter folgenden Adressen verfügbar ist:

Dem Installer kann per Kommandozeile aufgetragen werden, welche Teilkomponenten er laden und installieren soll.
Für VC++ sind die Workload-Pakete Microsoft.VisualStudio.Workload.MSBuildTools und Microsoft.VisualStudio.Workload.VCTools erforderlich. Zusätzlich wollen wir, dass auch “empfohlene” Zusatzpakete installiert werden, womit wir die Windows SDK und CMake erhalten.

Das Dockerfile hierfür sieht wie folgt aus:

 1# escape=`
 2
 3# Select the required Windows Server Release:
 4#FROM mcr.microsoft.com/windows/servercore:ltsc2016
 5#FROM mcr.microsoft.com/windows/servercore:ltsc2019
 6FROM mcr.microsoft.com/windows/servercore:ltsc2022
 7
 8SHELL ["cmd", "/S", "/C"]
 9
10RUN `
11  curl -SL --output vs_buildtools.exe `
12    https://aka.ms/vs/15/release/vs_buildtools.exe `
13  && (start /w `
14    vs_buildtools.exe --quiet --wait --norestart --nocache `
15    --installPath `
16      "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\BuildTools" `
17    --add Microsoft.VisualStudio.Workload.MSBuildTools `
18    --add Microsoft.VisualStudio.Workload.VCTools `
19    --includeRecommended `
20  || IF "%ERRORLEVEL%"=="3010" EXIT 0) `
21  && del /q vs_buildtools.exe
22
23ENTRYPOINT [ `
24"C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", `
25"&&", "cmd.exe", "/S", "/C"]

Mit dem Aufruf von
docker build -t msvc-build:2017 -f Dockerfile . im Verzeichnis der Datei wird das Docker-Image erstellt und mit dem TAG msvc-build:2017 abgelegt.

Dockerfile lädt den Visual Studio 2017 Installer herunter, startet ihn über das start Kommando und wartet bis die Installation fertig ist.

Als ENTRYPOINT wird das Visual Studio-Environment Script geladen, damit alle notwendigen Tools in die Environment-Variablen geladen werden.

Source Build Script

Den zu bauenden Quellcodes sollte ein Script beiliegen, das den gesamten Kompiliervorgang verwalten kann.
Wichtig ist dabei zu wissen, wo das Build-Script am Ende seine Daten ablegt, denn von der Stelle muss das Folgescript die fertigen Binaries abholen können.

Am einfachsten ist es, im Stammverzeichnis des Projektes eine Datei namens build.bat abzulegen.

Beispiel Visual Studio Solution

Wer eine Visual Studio Solution (*.sln) mit den Quellen in GIT eingecheckt hat, braucht nur msbuild in der Batch Datei aufzurufen.

1msbuild .\my-project.sln /t:Rebuild /p:configuration=release /p:platform=x64 /m

Die Binärdateien sollten dann in .\x64\Release ausgegeben werden.

Im Falle von 32-Bit Kompilaten, liegen sie in .\Release, wenn sie wie folgt erzeugt werden:

1msbuild .\my-project.sln /t:Rebuild /p:configuration=release /p:platform=win32 /m

Beispiel CMAKE

In CMakeLists.txt lässt sich per Variable definieren, wohin Binaries am Ende geschrieben werden sollen:

1set(MY_OUTPUT_PATH  "${CMAKE_BINARY_DIR}/output" )
2
3set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${MY_OUTPUT_PATH}" 
4    CACHE PATH "Shared library target directory")
5set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${MY_OUTPUT_PATH}" 
6    CACHE PATH "Static library target directory")
7set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${MY_OUTPUT_PATH}" 
8    CACHE PATH "Executable target directory")

Wenn wir CMake im Unterverzeichnis build des Source-Verzeichnis arbeiten lassen, werden dann die finalen Binärdateien in .\build\output\ zu finden sein.

1SET SOURCE_DIR=%~dp0
2SET BUILD_DIR=%SOURCE_DIR%\build
3CHDIR /d %SOURCE_DIR%
4RMDIR /s /q %BUILD_DIR%
5MKDIR %BUILD_DIR%
6CHDIR %BUILD_DIR%
7cmake -A x64 ..
8msbuild gate.sln /t:Rebuild /p:configuration=Release /p:platform=x64

Server Build Script

Auf dem Windows Server wird nun für jedes Projekt ein Server-Build Script angelegt, das wie folgt vorgeht:

  • Es legt ein temporäres Verzeichnis an
  • Die Quellcodes werden per GIT in das temporäre Verzeichnis geladen
  • Der Docker Container startet und bekommt das temporäre Verzeichnis als Austauschordner eingebunden.
  • Im Docker Container wird das Source-Build-Script ausgeführt, das alle Quellen zu Binärdateien übersetzt, die im mit dem Host geteilten temporärer Verzeichnis liegen.
  • Am Ende werden die erzeugten Binärdateien in ein Archiv gesichert und an einem dafür vorgesehenem Ort abgelegt.

Mein Beispiel Script sieht wie folgt aus:

 1SET GIT_URL=https://user:password@git-server.tld/path-to/project.git
 2SET DOCKER_IMAGE=msvc-build:2017
 3SET WORKDIR=%CD%
 4SET BUILD_ID=%RANDOM%
 5SET BUILD_DIR=%WORKDIR%\%BUILD_ID%
 6SET OUTPUT_ID=%DATE:~-4%-%DATE:~-10,2%-%DATE:~-7,2%_%TIME:~0,2%-%TIME:~3,2%-%TIME:~6,2%_%BUILD_ID%
 7SET OUTPUT_BASE=%WORKDIR%\artifacts
 8SET OUTPUT_DIR="%OUTPUT_BASE%\%OUTPUT_ID%"
 9DEL /F /S /Q %BUILD_DIR%
10MKDIR %BUILD_DIR%
11MKDIR %OUTPUT_BASE%
12MKDIR %OUTPUT_DIR%
13CHDIR %BUILD_DIR%
14
15git clone %GIT_URL% .
16
17docker run -v %BUILD_DIR%:c:\build %DOCKER_IMAGE% c:\build\build.bat
18
19tar -czvf %OUTPUT_DIR%\artifacts.tgz -C %BUILD_DIR%\build\output ./*.*
20
21CHDIR ..
22DEL /F /S /Q %BUILD_DIR%

Buildserver Produkte

GitLab CI

Ein Gitlab Server verwaltet GIT Repositories und weitere registrierte Build-Server (genannt “Runner”), auf denen der Gitlab Runner Dienst installiert ist. Jeder Runner wird durch ein TAG (eine Textmarke) adressiert und stellt eine bestimmte Umgebung (wie Docker, oder eine Shell) bereit.

In einer YAML Datei werden dann pro Projekt Details der Umgebung und ein Build-Script festgelegt, das bei einem Build auf einem Runner ausgeführt werden soll.

Gitlab Buildprozess

Gitlab Projekte können eine CI (Continuous Integration) YAML Datei namens .gitlab-ci.yml enthalten, die die Buildumgebung und notwendige Schritte beinhalten.

Der Buildprozess (auch Build Pipeline genannt) läuft dann so ab:

  • Beim Start lies Gitlab die CI-YAML Datei des Projektes und ermittelt über das enthaltene Runner-Tag auf welchem Runner-Server der Build laufen soll.
  • Der Runner Server lädt dann (z.B. per git clone) die Quellen herunter und startet die Umgebung (z.B. Docker) um darin das Build-Script auszuführen, das im CI-YAML definiert ist (z.B: CMake ausführen).
  • Am Ende definiert die CI-YAML Datei auch eine Liste von Dateimustern, welche Dateien aus dem Build-Prozess auf den Gitlab Server hochgeladen werden sollen (sogenannte Build-Artefakte).
    Dort stehen sie dann als Download zur Verfügung.
flowchart LR subgraph GIT[GITLAB Project] PROJECT[(GITLAB
Project
Repository)] YAML[[.gitlab_ci.yml]] YAMLARTIFACT(YAML
artifacts:
section) subgraph BUILD[Build Process Definition] YAMLSCRIPT(YAML
scripts:
section) SCRIPTS[[other_build_scripts.sh]] CMAKE[[CMakeLists.txt]] SOURCES[[Sources
*.h
*.hpp
*.c
*.cpp]] end end subgraph CI Runner RUNNER((Gitlab Runner
Service)) DOCKER(Docker
Container) end GITLAB[(GITLAB
Artifacts)] PROJECT --- YAML YAML --- YAMLSCRIPT YAML --- YAMLARTIFACT YAMLSCRIPT --script:
cmake --build--> CMAKE YAMLSCRIPT --script:
./run.sh--> SCRIPTS SCRIPTS --compile--> SOURCES CMAKE --compile--> SOURCES PROJECT --git clone--> RUNNER RUNNER --execute
YAML scripts--> DOCKER DOCKER --extract
artifacts--> YAMLARTIFACT YAMLARTIFACT --Upload
Build Results---> GITLAB

.gitlab-ci.yml Beispieldatei

 1# Liste von erlaubten "build stages"
 2# "Stages" beinhalten "Jobs", die hintereinander ausgeführt 
 3# werden. Wenn ein Job fehlschlägt, wird die Pipeline 
 4# abgebrochen und nachfolgende Stages werden standardgemäß 
 5# nicht mehr ausgeführt.
 6stages:
 7  - "build"
 8
 9# Ein "Job" in der Stage namens "build", der auf einem Runner
10# ausgeführt werden soll, der mit dem TAG "my_runner_tag" 
11# registriert wurde. Er soll das Docker Image mit dem Pfad 
12# "my_docker_image_path:1.2" herunterladen und das nachfolgende
13# Script darin ausführen.
14# Am Ende wird der Inhalt des Unterverzeichnis "build-output/" 
15# als Artefakt auf den Gitlab Server hochgeladen
16my_job1:
17  stage: "build"
18  tags:
19    - "my_runner_tag"
20  image: "my_docker_image_path:1.2"
21  script:
22    # scripts werden in dem Verzeichnis ausgeführt,
23    # in dem "git clone" das Projekt heruntergeladen hat.
24    - echo "Starting"
25    - make -j4
26    - echo "Finished"
27  artifacts:
28    paths:
29      - "build-output/*"

Fortsetzung folgt