Finalize workshop including development environment and presentation.

This commit is contained in:
Bart Beumer 2025-07-27 08:32:28 +00:00
parent f843ea2c13
commit df580b8930
30 changed files with 779 additions and 30 deletions

37
.clang-format Normal file
View File

@ -0,0 +1,37 @@
---
AlignAfterOpenBracket: Align
AllowAllArgumentsOnNextLine: 'false'
AllowAllConstructorInitializersOnNextLine: 'false'
AllowAllParametersOfDeclarationOnNextLine: 'false'
AllowShortBlocksOnASingleLine: 'false'
AllowShortCaseLabelsOnASingleLine: 'false'
AllowShortFunctionsOnASingleLine: InlineOnly
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: None
AllowShortLoopsOnASingleLine: 'false'
AlwaysBreakTemplateDeclarations: 'Yes'
BinPackArguments: 'false'
BinPackParameters: 'false'
BreakBeforeBraces: Allman
BreakConstructorInitializers: BeforeComma
ColumnLimit: '120'
Cpp11BracedListStyle: 'true'
FixNamespaceComments: 'true'
IncludeBlocks: Merge
IndentCaseLabels: 'true'
IndentWidth: '4'
Language: Cpp
MaxEmptyLinesToKeep: '2'
NamespaceIndentation: Inner
PointerAlignment: Left
SortUsingDeclarations: 'true'
SpaceAfterTemplateKeyword: 'false'
SpacesInAngles: 'false'
SpacesInCStyleCastParentheses: 'false'
SpacesInContainerLiterals: 'false'
SpacesInSquareBrackets: 'false'
Standard: Cpp11
TabWidth: '4'
UseTab: Never
...

View File

@ -3,18 +3,9 @@ FROM alpine:3.21.3
# Install tools required for building. # Install tools required for building.
RUN apk update && \ RUN apk update && \
apk add --no-cache \ apk add --no-cache \
autoconf \ autoconf bash build-base cmake gdb \
bash \ git libstdc++ libtool linux-headers \
build-base \ m4 perl python3 py3-pip\
cmake \ && \
gdb \
git \
libstdc++ \
libtool \
linux-headers \
m4 \
perl \
python3 \
py3-pip && \
pip install --break-system-packages conan && \ pip install --break-system-packages conan && \
conan profile detect conan profile detect

View File

@ -1,5 +1,5 @@
{ {
"name": "C++", "name": "Workshop Modern C++ Dev",
"build": { "build": {
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },

View File

@ -0,0 +1,7 @@
[requires]
boost/1.84.0
gtest/1.14.0
[generators]
CMakeDeps
CMakeToolchain

View File

@ -0,0 +1,19 @@
import os
from conan import ConanFile
from conan.tools.files import copy
from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps
class HelloConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
requires = "boost/1.84.0", "gtest/1.14.0", "libmagic/5.45"
generators = "CMakeDeps"
build_policy = "*"
def generate(self):
# We need to find the folder of libmagic and supply it to cmake so that
# we can deploy the magic file.
libmagic = self.dependencies["libmagic"]
tc = CMakeToolchain(self)
tc.variables["CONAN_LIBMAGIC_PACKAGE_FOLDER"] = libmagic.package_folder
tc.generate()

Binary file not shown.

View File

@ -0,0 +1,366 @@
\documentclass[aspectratio=169]{beamer}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{graphics}
\usepackage{graphicx}
\usepackage{verbatim}
\usepackage{plantuml}
\usepackage{listings}
\usepackage{csquotes}
\usepackage{multicol}
\usepackage{menukeys}
% Style related code
%
\setbeamerfont{footnote}{size=\tiny}
% Code block style
%
\lstset{
basicstyle=\footnotesize\ttfamily,
columns=fullflexible,
frame=single,
numbers=left
}
\title{Workshop Modern C++ build environment}
\author{Bart Beumer (bart.beumer@alten.nl)}
\institute{Alten}
\date{2025-07-29}
\begin{document}
\begin{frame}
\titlepage
\end{frame}
\begin{frame}{Rationale}
Using modern tools in combination with C \& C++ we can describe the build environment, 3rd party libraries and executables to build.
We will be able to create a highly reproducable build environment regardless of the OS running on the developer PC (as long as it can run docker).
\vspace{5mm}
This workshop aims to demonstrate how to create this build environment, how to use the tools, introduce ourselves to this way of working using included demonstration projects.
\end{frame}
\begin{frame}{Tools}
We will be using a combination of tools, each having their own purpose.
\begin{itemize}
\item Dev containers \footnote{\url{https://containers.dev/}}
\begin{quote}
A development container (or dev container for short) allows you to use a container as a full-featured development environment.
\end{quote}
\item Conan \footnote{\url{https://conan.io/faq}}
Open source, decentralized and multi-platform package manager.
\item CMake \footnote{\url{https://cmake.org/about/}}
\begin{quote}
... an open source, cross-platform family of tools designed to build, test, and package software.
\end{quote}
\end{itemize}
\end{frame}
\begin{frame}{Dev containers}
Solves the problem of having an encapsulated environment containing all tools, files, configuration that makes up the development environment.
\vspace{5mm}
Supported by multiple tools like:
\begin{itemize}
\item Visual Studio Code \footnote{\url{https://containers.dev/supporting}}
\item IntelliJ IDEA \footnote{\url{https://blog.jetbrains.com/idea/2024/07/using-dev-containers-in-jetbrains-ides-part-1/}}
\item GitHub codespaces \footnote{\url{https://docs.github.com/en/codespaces/about-codespaces/what-are-codespaces}}
\end{itemize}
\end{frame}
\begin{frame}{Dev containers}
We need the following files:
\begin{itemize}
\item Dockerfile
\item devcontainer.json
\end{itemize}
\end{frame}
\begin{frame}{Dev containers}
Using docker compose we create the entire container.
\lstinputlisting[keepspaces,firstline=1, firstnumber=1]{../.devcontainer/Dockerfile}
\end{frame}
\begin{frame}{Dev containers}
A devcontainer.json contains metadata used by tools.
\lstinputlisting[firstline=1, firstnumber=1]{../.devcontainer/devcontainer.json}
\end{frame}
\begin{frame}{Dev containers}
We will experiment with these files during the workshop.
\end{frame}
\begin{frame}{Conan}
\begin{quotation}
Conan is a package manager for C and C++ which aims to solve some very common and difficult challenges.\footnote{\url{https::/conan.io/faq}}
\end{quotation}
\begin{itemize}
\item Conveniently depend on 3rd party packages and use them.
\item Automates the process of downloading, building \& deploy for further use.
\item Provide a mechanism to use libraries.
\end{itemize}
\end{frame}
\begin{frame}{Conan}
A simple text file can be used to configure dependencies on packages.
\lstinputlisting[language=make]{../conanfile.txt}
\end{frame}
\begin{frame}{Conan}
We will experiment with these files during the workshop.
\end{frame}
\begin{frame}{CMake}
\begin{quote}
CMake is an open source, cross-platform family of tools designed to build, test, and package software. CMake gives you control of the software compilation process using simple independent configuration files. Unlike many cross-platform systems, CMake is designed to be used in conjunction with the native build environment.
\footnote{\url{https://cmake.org/about/}}
\end{quote}
\begin{itemize}
\item Good documentation and tutorials provided.\footnote{\url{https://cmake.org/cmake/help/latest/guide/tutorial/index.html}}
\item Tools run locally on a system.
\end{itemize}
\end{frame}
\begin{frame}{CMake}
Using files we can describe libraries, executables, the (external) dependencies they have.
\lstinputlisting[language=make]{../src/hello_world/CMakeLists.txt}
\end{frame}
\begin{frame}{CMake}
We will experiment with these files during the workshop.
\end{frame}
\begin{frame}{Workshop}
\begin{multicols}{2}
Lets start with opening our IDE and our project folder.
\includegraphics[width=0.45\textwidth]{vscode_empty}
Quickly after opening the folder we get the following question:
\includegraphics[width=0.45\textwidth]{vscode_folder_contains_devcontainer}
VS Code will build the container if needed and start using it.
\end{multicols}
\end{frame}
\begin{frame}{Workshop}
The dev container has been built, now we need to let conan work and fetch and build dependencies.
\vspace{5mm}
\keys{CTRL + SHIFT + P} ``conan install''
\vspace{5mm}
\includegraphics[width=0.75\textwidth]{vscode_conan_install}
\end{frame}
\begin{frame}{Workshop}
\begin{multicols}{2}
We can now use CMake to build our software.
\includegraphics[height=0.65\textheight]{vscode_conan_done_cmake}
\begin{itemize}
\item configure
\item build
\item debug/launch
\end{itemize}
\end{multicols}
\end{frame}
\begin{frame}{Workshop}
Continuing with theory
\end{frame}
\begin{frame}[fragile]{Conan: important commands}
\begin{verbatim}
pip install conan
\end{verbatim}
We use the python package manager to install conan. (part of Dockerfile)
\begin{verbatim}
conan profile detect
\end{verbatim}
We let conan detect compilers and tools for which it can use. (part of Dockerfile)
\begin{verbatim}
conan install
\end{verbatim}
While in the project working directory, download packages, compile them, generate CMake environment. (was run using vs code)
After that we can use CMake as we would normally.
\end{frame}
\begin{frame}{CMake}
\end{frame}
\begin{frame}{CMake: The basics}
\lstinputlisting[language=make]{../src/hello_world/CMakeLists.txt}
\end{frame}
\begin{frame}{CMake: The basics}
\lstinputlisting[language=make,firstline=1,lastline=2]{../src/hello_world/CMakeLists.txt}
\begin{itemize}
\item Sets the minimum required version of cmake for a project.
\item Sets behavior of CMake as it was at a certain version (set policies)\footnote{\url{https://cmake.org/cmake/help/latest/command/cmake_minimum_required.html}}
\end{itemize}
\vspace{5mm}
A mechanism to provide backwards compatibility while allowing future versions to change behavior of commands.
\end{frame}
\begin{frame}[fragile]{CMake: The basics}
\lstinputlisting[language=make,firstline=2, firstnumber=2, lastline=9]{../src/hello_world/CMakeLists.txt}
\begin{itemize}
\item ``project''
Used to set the PROJECT\_NAME variable.
\item ``add\_executable''
Adding executable by specifying the executable name and the source files.
\end{itemize}
\end{frame}
\begin{frame}[fragile]{CMake: The basics}
\lstinputlisting[language=make,firstline=9, firstnumber=9, lastline=18]{../src/hello_world/CMakeLists.txt}
\begin{itemize}
\item ``set\_property''
Enables us to set properties like language standard to use (C++, CUDA, ...)
\end{itemize}
\end{frame}
\begin{frame}{CMake: The basics, library}
\lstinputlisting[language=make, firstline=3,firstnumber=3, lastline=21]{../src/awesome_log/CMakeLists.txt}
\end{frame}
\begin{frame}{CMake: The basics, library}
\lstinputlisting[language=make,firstline=5,firstnumber=5,lastline=11]{../src/awesome_log/CMakeLists.txt}
Similar to ``add\_executable'' we can create a library, specifying the name of the library and the sources that form it.
\end{frame}
\begin{frame}{CMake: The basics, library}
\lstinputlisting[language=make,firstline=18,firstnumber=18,lastline=21]{../src/awesome_log/CMakeLists.txt}
Add include directories, for both creating the target itself AND targets depending on this target.
\vspace{5mm}
No need for consumers of the library to set include paths, they inherit from this library target.
\end{frame}
\begin{frame}{Using 3rd-party packages}
\end{frame}
\begin{frame}{Conan: 3rd-party packages}
\lstinputlisting[language=make]{example_conan.txt}
\begin{itemize}
\item ``[requires]''
Declare the libraries that we want to use in our project.
\item ``[generators]''
Tell Conan to generate files for given build system
\end{itemize}
\end{frame}
\begin{frame}{Conan: 3rd-party packages the python way}
\lstinputlisting[language=make]{example_conan_py.txt}
\end{frame}
\begin{frame}{CMake: Using 3rd-party packages}
\lstinputlisting[language=make]{../src/example_boost/CMakeLists.txt}
\end{frame}
\begin{frame}{CMake, 3rd-party packages}
\lstinputlisting[language=make, firstline=2,firstnumber=2,lastline=4]{../src/example_boost/CMakeLists.txt}
Find a package (usually provided by something external to the project), and load its package-specific details.\footnote{\url{https://cmake.org/cmake/help/latest/command/find_package.html}}
\lstinputlisting[language=make, firstline=15,firstnumber=15,lastline=18]{../src/example_boost/CMakeLists.txt}
Using exposed target names we can use those packages.
\begin{itemize}
\item Affects libraries linked to target.
\item Affects include paths to look for headers.
\end{itemize}
\end{frame}
\begin{frame}[fragile]{CMake: Testing}
\begin{verbatim}
enable_testing()
\end{verbatim}
Use enable\_testing() at the top level CMakeLists.txt to enable cmake to generate things needed to support testing\footnote{\url{https://cmake.org/cmake/help/latest/command/enable_testing.html}}.
\begin{verbatim}
add_test()
\end{verbatim}
Use add\_test() inside your CMakeLists.txt to register the test\footnote{\url{https://cmake.org/cmake/help/latest/command/add_test.html}}.
\begin{verbatim}
ctest
\end{verbatim}
Commandline application to run all registered tests\footnote{\url{https://cmake.org/cmake/help/latest/manual/ctest.1.html}}.
\end{frame}
\begin{frame}
Worshop time! Suggestions:
\begin{itemize}
\item Play around in the environment, have a look at some of the projects.
\item Try and add a 3rd party package and use it (fmt, ...)
\item Use a different compiler.
\item Add a tool to your image (valgrind, ...).
\end{itemize}
\end{frame}
\end{document}

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,3 +1,6 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
add_subdirectory(example_boost) add_subdirectory(example_boost)
add_subdirectory(hello_world)
add_subdirectory(hello_world_awesome)
add_subdirectory(awesome_log)

View File

@ -0,0 +1,48 @@
cmake_minimum_required(VERSION 3.20)
project(awesome_log)
add_library(
${PROJECT_NAME}
./src/console_writer.cpp
./src/log.cpp
./src/null_writer.cpp
./src/scoped_log.cpp
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 20
)
target_include_directories(
${PROJECT_NAME}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
)
find_package(GTest REQUIRED)
project(awesome_log_test)
add_executable(
${PROJECT_NAME}
./test/test_scoped_log.cpp
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 20
)
target_link_libraries(
${PROJECT_NAME}
PUBLIC
awesome_log
GTest::gtest_main
GTest::gmock
)
add_test(
NAME ${PROJECT_NAME}
COMMAND ${PROJECT_NAME}
)

View File

@ -0,0 +1,14 @@
#pragma once
#include "writer_interface.hpp"
namespace awesome_log
{
class console_writer : public writer_interface
{
public:
~console_writer() override;
void log(std::string_view msg) override;
};
} // namespace awesome_log

View File

@ -0,0 +1,12 @@
#pragma once
#include <memory>
namespace awesome_log
{
class writer_interface;
writer_interface& get_writer();
void set_writer(const std::shared_ptr<writer_interface>& writer);
} // namespace awesome_log

View File

@ -0,0 +1,14 @@
#pragma once
#include "writer_interface.hpp"
namespace awesome_log
{
class null_writer : public writer_interface
{
public:
~null_writer() override;
void log(std::string_view msg) override;
};
} // namespace awesome_log

View File

@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <string_view>
namespace awesome_log
{
class scoped_log
{
public:
scoped_log(std::string_view msg);
~scoped_log();
scoped_log() = delete;
// No copying or moving allowed.
scoped_log(const scoped_log&) = delete;
scoped_log(scoped_log&&) = delete;
scoped_log& operator=(const scoped_log&) = delete;
scoped_log& operator=(scoped_log&&) = delete;
private:
std::string m_msg;
int m_uncaught_exceptions;
};
} // namespace awesome_log

View File

@ -0,0 +1,15 @@
#pragma once
#include <string_view>
namespace awesome_log
{
class writer_interface
{
public:
virtual ~writer_interface() = default;
virtual void log(std::string_view msg) = 0;
};
} // namespace awesome_log

View File

@ -0,0 +1,12 @@
#include <awesome_log/console_writer.hpp>
#include <iostream>
using namespace awesome_log;
console_writer::~console_writer() = default;
void console_writer::log(std::string_view msg)
{
std::cout << msg << std::endl;
}

View File

@ -0,0 +1,23 @@
#include <awesome_log/log.hpp>
#include <awesome_log/null_writer.hpp>
using awesome_log::writer_interface;
namespace
{
std::shared_ptr<writer_interface> g_instance;
}
writer_interface& awesome_log::get_writer()
{
if (!g_instance)
{
g_instance = std::make_shared<null_writer>();
}
return *g_instance;
}
void awesome_log::set_writer(const std::shared_ptr<writer_interface>& writer)
{
g_instance = writer;
}

View File

@ -0,0 +1,10 @@
#include <awesome_log/null_writer.hpp>
using namespace awesome_log;
null_writer::~null_writer() = default;
void null_writer::log(std::string_view)
{
}

View File

@ -0,0 +1,32 @@
#include <awesome_log/log.hpp>
#include <awesome_log/scoped_log.hpp>
#include <awesome_log/writer_interface.hpp>
#include <exception>
#include <sstream>
using namespace awesome_log;
scoped_log::scoped_log(std::string_view msg)
: m_msg(msg)
, m_uncaught_exceptions(std::uncaught_exceptions())
{
std::stringstream ss;
ss << "> " << m_msg;
get_writer().log(ss.str());
}
scoped_log::~scoped_log()
{
bool is_stack_unwinding = (m_uncaught_exceptions != std::uncaught_exceptions());
std::stringstream ss;
ss << "< " << m_msg;
if (is_stack_unwinding)
{
ss << " (stack unwinding)";
}
get_writer().log(ss.str());
}

View File

@ -0,0 +1,13 @@
#pragma once
#include <awesome_log/writer_interface.hpp>
#include <gmock/gmock.h>
class mock_writer : public awesome_log::writer_interface
{
public:
~mock_writer() override = default;
MOCK_METHOD(void, log, (std::string_view), (override));
};

View File

@ -0,0 +1,47 @@
#include "mock_writer.hpp"
#include <awesome_log/log.hpp>
#include <awesome_log/scoped_log.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
class test_scoped_log : public ::testing::Test
{
public:
void SetUp() override
{
m_writer = std::make_shared<mock_writer>();
awesome_log::set_writer(m_writer);
}
void TearDown() override { awesome_log::set_writer(std::shared_ptr<awesome_log::writer_interface>()); }
protected:
std::shared_ptr<mock_writer> m_writer;
};
TEST_F(test_scoped_log, log_normal_flow)
{
EXPECT_CALL(*m_writer, log("> log"));
EXPECT_CALL(*m_writer, log("< log"));
{
awesome_log::scoped_log a("log");
}
}
TEST_F(test_scoped_log, log_stack_unwind)
{
EXPECT_CALL(*m_writer, log("> log"));
EXPECT_CALL(*m_writer, log("< log (stack unwinding)"));
try
{
awesome_log::scoped_log a("log");
throw std::runtime_error("");
}
catch (const std::exception& e)
{
// Not doing anything
}
}

View File

@ -2,18 +2,16 @@ cmake_minimum_required(VERSION 3.20)
find_package(Boost 1.88.0 REQUIRED COMPONENTS serialization) find_package(Boost 1.88.0 REQUIRED COMPONENTS serialization)
project(hello_world_boost) project(example_boost)
add_executable( add_executable(
${PROJECT_NAME} ${PROJECT_NAME}
./src/main.cpp ./src/main.cpp
) )
set_property( set_property(
TARGET ${PROJECT_NAME} TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 20 PROPERTY CXX_STANDARD 20
) )
target_link_libraries( target_link_libraries(
${PROJECT_NAME} ${PROJECT_NAME}
Boost::serialization Boost::serialization

View File

@ -1,31 +1,27 @@
#include <sstream>
#include <iostream>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp> #include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <iostream>
#include <sstream>
struct position struct position
{ {
double x; int x;
double y; int y;
template<class Archive> template<class Archive>
void serialize(Archive & ar, const unsigned int /*version*/) void serialize(Archive& ar, const unsigned int /*version*/)
{ {
ar & x; ar & x;
ar & y; ar & y;
} }
bool operator==(const position& rhs) const bool operator==(const position& rhs) const { return x == rhs.x && y == rhs.y; }
{
return x == rhs.x
&& y == rhs.y;
}
}; };
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
position myposition{.x=123.0, .y=456.0}; position myposition{.x = 123, .y = 456};
std::stringstream ss; std::stringstream ss;
{ {
@ -44,5 +40,4 @@ int main(int argc, char* argv[])
{ {
std::cout << "positions are the same" << std::endl; std::cout << "positions are the same" << std::endl;
} }
} }

View File

@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.20)
project(hello_world)
add_executable(
${PROJECT_NAME}
src/main.cpp
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 20
)

View File

@ -0,0 +1,7 @@
#include <iostream>
int main(int argc, char* argv[])
{
std::cout << "Hello world" << std::endl;
}

View File

@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.20)
project(hello_world_awesome)
add_executable(
${PROJECT_NAME}
src/main.cpp
)
target_link_libraries(
${PROJECT_NAME}
awesome_log
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 20
)

View File

@ -0,0 +1,28 @@
#include <awesome_log/console_writer.hpp>
#include <awesome_log/log.hpp>
#include <awesome_log/scoped_log.hpp>
#include <iostream>
#include <memory>
using awesome_log::scoped_log;
int main(int argc, char* argv[])
{
awesome_log::set_writer(std::make_shared<awesome_log::console_writer>());
{
scoped_log scoped("main.cpp, printing text");
std::cout << "Hello world" << std::endl;
}
try
{
scoped_log scoped("main.cpp, playing with exceptions");
throw std::runtime_error("Let's play!");
// ... unreachable code goes here.
}
catch (const std::exception& e)
{
std::cout << "Caught an exception, as expected in this example" << std::endl;
}
}