A ready-to-use C++20 project template: CMake, clang++, ccache, libutils, exception headers generated from JSON, GoogleTest, Linux and Windows (MinGW-w64) builds, RPM / DEB / Arch (pacman) packages with the libutils naming and the libutils workflows (dispatch, unit tests, packages, GitHub release, gh-pages repository and documentation).
The setup.sh script downloads it, renames the placeholders (template and Core) to your project name and core class name, adapts it to the project kind (binary, library, header-only), writes the README / CHANGELOG / documentation, optionally creates the gh-pages branch, and can commit/push the result.
Caution
This project's license does not apply to the content of the dependencies used by the template.
| Name + Link | Status | Last Update |
|---|---|---|
| libutils |
Build requirements:
| Name | Version | Fedora (dnf) |
Debian/Ubuntu (apt) |
Arch (pacman) |
|---|---|---|---|---|
clang++ / cmake |
C++20 / >= 3.20 |
clang cmake |
clang cmake |
clang cmake |
python3 |
>= 3.8 |
python3 |
python3 |
python |
libutils |
4.x (>= 4.1.0, >= 4.2.0 on Arch) |
libutils |
libutils |
libutils |
| GoogleTest (tests only) | any | gtest-devel |
libgtest-dev |
gtest |
| MinGW-w64 (Windows only) | GCC >= 13 |
mingw64-gcc-c++ mingw64-libutils |
g++-mingw-w64-x86-64 + libutils built for MinGW |
— |
rpm-build, dpkg, bsdtar (packages only, bsdtar for the Arch ones) |
any | rpm-build dpkg bsdtar |
rpm dpkg-dev libarchive-tools |
rpm-tools dpkg libarchive |
ccache [OPTIONAL] |
any | ccache |
ccache |
ccache |
Tip
libutils packages (Linux libutils*, Windows mingw64-libutils*) and source install: see the libutils README.
Setup into the current directory
Important
With --push, the script must be run from inside an existing Git repository with a configured remote: it finishes with git add . && git commit && git push (and git push origin gh-pages when the branch is created).
Warning
<project_name> and <core_name> are used as C++ namespace and class name: they must be valid C++ identifiers ([A-Za-z_][A-Za-z0-9_]*), ex: my_project, not my-project. testing / Test are refused (GoogleTest namespace).
- Clones the latest version of the template (
mainbranch) into a temporary directory - Asks the project kind and the
gh-pagesquestion in the terminal when they are not given as flags (defaults:binary, nogh-pages, also without a terminal) - Removes the template only files (
setup.sh,.github/workflows/setup.yml,gh-pages/) and adapts the copy to the kind (TEMPLATE_KINDof the CMake,PACKAGE_*of the workflows, nomain.cppfor a library, nosrc/for a header-only library) - Renames every file or directory containing a placeholder (ex:
src/template/Core.cpp->src/my_project/App.cpp) and replaces every placeholder inside the sources, headers, CMake files, workflows, scripts,.gitignoreand theDoxyfile(template,TEMPLATE_,Core,CORE_H, the repositoryTsukiNi22/<project_name>from theoriginremote orGITHUB_REPO) - Writes the
README.md(dependencies, packages, setup, usage, workflows of the project), theCHANGELOG.md(first entryv1.0.0: kind, date, repository; the template repository has no changelog of its own) and the documentationdocs/(technical page placeholder, project graph pointing at the new repository) - With
gh-pages: keeps the package repository setup script assetup.sh, thedocs.ymlworkflow and the publication job ofbuild-packages.yml, and creates thegh-pagesbranch (never touching the working tree) with the packages repository seed (<project_name>.repo,RPM-GPG-KEY-tsukini,Sync Packages (CD)workflow;fedora/,debian/andarch/filled by the first publication), the documentation (docs/) and the rootindex.htmlredirecting to it. Without: no publication, the packages stay as artifacts of the workflow runs - With
--push: commits and pushes the result (chore: setup c++20 project from cpp_project_template)
wget -qO- https://raw.githubusercontent.com/TsukiNi22/cpp_project_template/main/setup.sh | bash -s -- <project_name> <core_name> --kind binary --gh-pages --pushor with curl:
curl -fsSL https://raw.githubusercontent.com/TsukiNi22/cpp_project_template/main/setup.sh | bash -s -- <project_name> <core_name> --kind binary --gh-pages --push| Argument | Effect |
|---|---|
<project_name> |
Replaces template (target, namespace, include//src/ folders, executable, packages) |
<core_name> |
Replaces Core (main class and its files) |
--kind <kind> |
Project kind: binary (default), library (.a / .so + headers) or header-only (asked in the terminal when missing) |
--gh-pages / --no-gh-pages |
Create (or not) the gh-pages branch and keep its workflows (asked in the terminal when missing, no without a terminal) |
--push (-push) |
Commits and pushes the result (skipped by default) |
| Environment variable | Effect |
|---|---|
REPO_URL |
Template repository to clone (default: https://github.com/TsukiNi22/cpp_project_template.git) |
BRANCH |
Branch to clone (default: main) |
TEMPLATE_DIR |
Local copy of the template used instead of the clone (used by the CI) |
GITHUB_REPO |
owner/repo of the new project (default: the origin remote, else TsukiNi22/<project_name>): workflows, docs, package repository URL |
| Path | Content |
|---|---|
CMakeLists.txt |
Options at the top (kind, package content), libutils, exception header generation, release / install_release / package_release targets, CPack |
cmake/toolchain/mingw-w64.cmake |
Windows (x86_64) cross compilation toolchain: cmake -S . -B build-windows -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/mingw-w64.cmake |
cmake/package/ |
Config.cmake.in (find_package(<project_name>), checks the installed variant), CPackProjectConfig.cmake (MinGW sysroot prefix of the Windows packages) |
cmake/config/exceptions/*.json, cmake/scripts/ |
Project exception codes (utils::exception::ExternalCode) and their generator (generate_exception_header.py, const.py), check_files.sh |
src/<project_name>/, include/<project_name>/ |
main.cpp (entry, exception handling), <Core>-init.cpp (libutils ArgParser, --verbose), <Core>.cpp (run), the public header |
tests/ |
GoogleTest unit tests (-DBUILD_TESTS=ON, unit_tests at the root, GTest fetched from the sources when cross compiling) |
docs/ |
HTML documentation: index.html (technical page, placeholder "coming soon"), graph.html (interactive project graph, refreshed from the repository), logo.svg |
.github/workflows/ |
dispatch.yml, unit-tests.yml, build.yml, build-packages.yml, release.yml, docs.yml (see Workflows) |
setup.sh (gh-pages only) |
Package repository setup of the project (mirror + GPG key + install, --pre, --windows, --no-install, --no-sudo), from install.sh of the template |
README.md, CHANGELOG.md (both written by setup.sh), LICENSE (MIT), Makefile, Doxyfile, .gitignore |
Project files |
The options are at the very top of the CMakeLists.txt (<NAME> = project name in upper case):
| Option | Default | Content |
|---|---|---|
<NAME>_KIND |
the --kind |
binary, library or header-only: sets the defaults of the three package options below |
<NAME>_PACKAGE_BIN |
ON for binary |
Build and package the executable (<name>-op / -db / -as) |
<NAME>_PACKAGE_LIB |
ON for library |
Build and package the library (lib<name>-op / -db / -as, lib<name>.a / _debug.a / _asan.a) |
<NAME>_PACKAGE_DEV |
ON for library, header-only |
Package the public headers and the CMake config (lib<name>-dev, -dev-op / -db / -as with a library) |
<NAME>_SHARED_LIB |
OFF |
Shared library (.so / .dll) instead of a static one (.a) |
<NAME>_STABLE_RELEASE |
OFF |
Package channel: stable (ON) or pre-release -pre (OFF) |
<NAME>_DEV_PACKAGE, <NAME>_PACKAGE |
OFF |
Internal: the headers packages / the global package build (set by package_release) |
BUILD_TESTS |
OFF |
Build the unit tests |
Note
A binary can also ship its library and headers: -D<NAME>_PACKAGE_LIB=ON -D<NAME>_PACKAGE_DEV=ON (the executable is linked on the library target <name>_lib). After a change of these options, update PACKAGE_BIN / PACKAGE_LIB / PACKAGE_DEV in .github/workflows/build.yml and build-packages.yml.
Note
Every package has a pre-release/unstable version named <package_name>-pre.
The -pre packages are marked as obsolete by any release/stable package (without -pre) of the same version or higher.
One version for the whole project (project(... VERSION X.Y.Z)), the same naming as libutils (<name> = project name, lib<name> = the library, never liblib...):
| File Name | Content | With |
|---|---|---|
<name> / lib<name> |
Global package: installs the default packages (-op variants, -dev) |
always |
<name>-op |
Executable compiled with the optimization options | PACKAGE_BIN |
<name>-db |
Executable compiled with the debug options (suggests gdb) |
PACKAGE_BIN |
<name>-as |
Executable compiled with the asan options | PACKAGE_BIN |
lib<name>-op |
Library compiled with the optimization options (lib<name>.a) |
PACKAGE_LIB |
lib<name>-db |
Library compiled with the debug options (lib<name>_debug.a) |
PACKAGE_LIB |
lib<name>-as |
Library compiled with the asan options (lib<name>_asan.a) |
PACKAGE_LIB |
lib<name>-dev |
All the includes and the global CMake configuration (requires libutils-dev) |
PACKAGE_DEV |
lib<name>-dev-op / -db / -as |
CMake configuration of each library variant (requires libutils-op / -db / -as) |
PACKAGE_DEV + PACKAGE_LIB |
Warning
The three variants of the executable install the same /usr/bin/<name>: they conflict with each other, only one is installed at a time (the libraries are installed side by side, like libutils).
Windows (MinGW-w64 cross compilation, noarch packages installed in the MinGW sysroot: /usr/x86_64-w64-mingw32/sys-root/mingw on Fedora, /usr/x86_64-w64-mingw32 on Debian/Ubuntu): the same packages prefixed with mingw64- (mingw64-<name>, mingw64-lib<name>-dev...), without the -as variants (no AddressSanitizer with MinGW), requiring mingw64-libutils* / mingw64-openssl.
| Build type | Package suffix | Options |
|---|---|---|
Debug |
-db |
-g -ggdb3 |
Asan |
-as |
-fsanitize=address -fno-omit-frame-pointer -g -ggdb3 (Linux only) |
Optimized |
-op |
-O3 -ffast-math -funroll-loops -fno-plt -pipe -DNDEBUG (no -march=native: distributed) |
Of the template itself (the template does not compile as is: template is a C++ keyword, run setup.sh first) and of a generated project:
make # Configure & build into build/ (executable at the root)
make re # Full rebuild
make fclean # Remove the build directory
export BUILD_DIR=build
cmake -S . -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Debug # Debug build (-g -ggdb3)
cmake -S . -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Asan # AddressSanitizer build
cmake --build $BUILD_DIR --target release # Debug, Asan & Optimized builds
cmake --build $BUILD_DIR --target install_release # Install the three variants (library) / the executable
cmake --build $BUILD_DIR --target package_release # RPM / DEB / Arch of every variant + headers + global (build/*.rpm, *.deb, *.pkg.tar.zst)
cmake --build $BUILD_DIR --target get_unregistered_files # .cpp files missing from the SRC / SRC_SHARED lists
cmake -S . -B $BUILD_DIR -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug && cmake --build $BUILD_DIR && (cd $BUILD_DIR && ctest)
export BUILD_DIR=build-windows # Windows (x86_64) from Linux, mingw64-libutils installed
cmake -S . -B $BUILD_DIR -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/mingw-w64.cmake -DCMAKE_BUILD_TYPE=Optimized
cmake --build $BUILD_DIR --parallel $(nproc) # <name>.exe at the root
cmake --build $BUILD_DIR --target package_release # mingw64-<name>* RPM / DEB
WINEPATH=/usr/x86_64-w64-mingw32/sys-root/mingw/bin wine ./<name>.exe --help
./<project_name> --help # Generated help (libutils ArgParser)
./<project_name> -v debug # Verbose level: none|basic|advanced|debugNote
Every new .cpp must be added to the SRC (executable only) or SRC_SHARED (executable and library) list of the CMakeLists.txt (get_unregistered_files lists the missing ones). Portable code only for Windows: system calls through utils::platform::, <windows.h> before <utils/utils.hpp>.
CI - Setupruns on every push onmainof the template repository: it runs thesetup.shof the pushed commit for each kind (binary,library,header-only, with--gh-pages) and once for Windows (--no-gh-pages), checks the result (no placeholder left, generated files,gh-pagesbranch content), builds the project, runs its unit tests and builds its packages
Dispatch (CI/CD): runs on every push (branchmainor tagv*, not for*.md/*.txt/docs/only changes) and decides what to trigger from the ref / commit messageUnit Tests (CI): builds and runs the unit tests, always first (Linux in theunit-testsimage, Windows in themingw64-unit-testsimage underwine)Build (CI): only builds the project (Linux in theciimage, Windows in themingw64-ciimage) and checks the outputs (executable / libraries), without producing packagesBuild - Packages (CI/CD): builds the packages (RPM/DEB/Arch: Linux in theciimage, Windowsmingw64-*in themingw64-ciimage), checks them against thePACKAGE_*content, then (gh-pages only) signs and publishes them on thegh-pagesrepository (fedora/44/x86_64,debian/,arch/x86_64, 5 last-preversions kept)Release - GitHub (CD): creates the GitHub release of a tag from its section of theCHANGELOG.md(## vX.Y.Z - date/## [vX.Y.Z-pre] - date), once the packages are built / publishedDocs - Pages (CD)(gh-pages only): copiesdocs/on thegh-pagesbranch on every push changing it (https://<owner>.github.io/<repo>/)Sync Packages (CD)(on thegh-pagesbranch): rebuilds the metadata and signatures of the package repository after a manual push on the branch (not the bot / docs commits)
Note
Build (CI) only runs when Dispatch (CI/CD) decides not to build the packages, to check that the code still compiles. The disabled notify-downstream job of dispatch.yml is an example of repository_dispatch to another repository (like libutils -> docker-image).
Important
The workflows run on the runner set by the repository variable RUNNER (gh variable set RUNNER --body <runner>), inside the containers ghcr.io/tsukini22/ci, unit-tests, mingw64-ci, mingw64-unit-tests, package and git (libutils 4.x inside). The publication needs the secrets GPG_PRIVATE_KEY (base64), GPG_KEY_ID, GPG_PASSPHRASE, and GitHub Pages enabled on the gh-pages branch (gh api -X POST repos/<owner>/<repo>/pages -f 'source[branch]=gh-pages' -f 'source[path]=/').
Tip
A commit containing [ignore] skips the whole CI.
The pre-release of the packages (<package_name>-pre) is triggered by:
- a tag with a suffix, matching
vx.x.x-<suffix>(ex:v1.0.1-pre,x=major,minor,fix) - or a commit containing
[build]
The release of the packages (channel stable) and the GitHub release are only triggered by a tag matching exactly vx.x.x (ex: v1.0.0), whose section must exist in the CHANGELOG.md.
A commit containing [release] (or a package publication) is the trigger of the commented notify-downstream job of Dispatch (CI/CD).