Contents

My C++ setup for personal projects

Here’s a quick overview of my repo setup for any new C++ projects that I create. I’m gonna start with an empty repo and bring it up to an initial stage where all my preferred tooling is available and ready.

Empty repo scaffolding

Here’s the link to the repo, if you’re not interested in the walk through.

Build system

Starting with an empty git repo, first thing I like to do is just to create main.cpp with an empty main function. I use vim-luasnips so, creating that is instantaneous.

By now, I’m sure you know that I like meson and for personal projects it’s perfect! So, you guessed it, I establish the build system:

1
meson init -l cpp .

This will create meson.build which is a good starting point. Sometimes, you need to adjust the filenames used to create the executable so, I do that. Having the build system, I usually bootstrap the build directory and link the compilation database:

1
2
3
meson setup bld
ln -sf bld/compile_commands.json .
echo bld >>.gitignore

I mostly use bld as the build directory so, I add it to .gitignore and commit the link to compile_commands.json to the repo for convenience. This concludes my first commit:

1
2
git add main.cpp .gitignore meson.build compile_commands.json
git commit -m "chore: initial"

Having the compile_commands.json automatically updated by meson means that LSP within my editor will always work, which is great!

Pre-commit hooks

Right, since this is a C++ project… I’ll continue with Python poetry. Wait, what?

Yes, that’s not a mistake. Usually people have their own pre-commit scripts or install them manually, but I decided that it’s not really worth to be a purist and reinvent the wheel as far as that goes. Python tooling is good and it save a lot of manual steps. I realise that this may not be a preferred way for everyone, it does work for me though so, hear me out :).

Okay, disclaimer aside, let’s continue. Yep, Python poetry:

1
poetry -q init

This will create pyproject.toml. The contents are not really that important as I mostly gonna use it as a development tools package manager. Having that, let’s install some basic tools. First pre-commit hook:

1
poetry add --group dev pre-commit

For conventional commits support, I’ll need commitizen:

1
poetry add --group dev commitizen

With that done, it’s time to create pre-commit config file:

1
touch `.pre-commit-config.yaml`

Here’s my preferred set of hooks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.4.0
  hooks:
  - id: trailing-whitespace
  - id: end-of-file-fixer
  - id: check-added-large-files

- repo: https://github.com/pocc/pre-commit-hooks
  rev: v1.3.5
  hooks:
  - id: clang-format
    stages: [pre-commit]
  - id: clang-tidy
    stages: [pre-commit]

- repo: https://github.com/commitizen-tools/commitizen
  rev: 3.2.1
  hooks:
  - id: commitizen
    stages: [commit-msg]

First three (trailing-whitespace, end-of-line-fixer, check-added-large-files) are pretty standard, just to prevent polluting the repo with white space noise and rubbish binary files.

I like to use hooks from github/pocc which integrate clang tools. Since clang requires gcc ang libstdc++, I don’t install it via poetry but rather rely on versions provided system wide, managed by OS package manager:

# Arch linux
pacman -S clang

# macOS
brew install llvm

Your mileage may vary but hopefully you get the gist.

Additionally, I recently became a fan of conventional commits so, I’m integrating a tool to validate my commits against that as well.

Now it’s time to install the hooks:

1
2
poetry run pre-commit install --hook-type pre-commit
poetry run pre-commit install --hook-type commit-msg

Since, pre-commit is run locally, re-installation of hooks is required on every freshly cloned repo so, to ease the burden of doing this, I usually add a small script to the repo:

1
2
3
mkdir scripts
touch scripts/setup_env.sh
chmod +x scripts/setup_env.sh

The contents of which are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

# install tools in venv
poetry install

# install pre-commit hooks
poetry run pre-commit install --hook-type pre-commit
poetry run pre-commit install --hook-type commit-msg

# setup build directory
meson setup bld

All of that concludes the second commit:

1
2
git add .pyproject.toml poetry.lock .pre-commit-config.yaml scripts
git commit -m "chore: setup pre-commit"

This already ran all the configured hooks which is great!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ git commit -m "chore: setup pre-commit"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check for added large files..............................................Passed
clang-format.........................................(no files to check)Skipped
clang-tidy...........................................(no files to check)Skipped
commitizen check.........................................................Passed
[master 9787be7] chore: setup pre-commit
 4 files changed, 229 insertions(+)
 create mode 100644 .pre-commit-config.yaml
 create mode 100644 poetry.lock
 create mode 100644 pyproject.toml
 create mode 100644 scripts/setup_env.sh

clang-format

Now, I’ve added .clang-format to pre-commit hooks but I didn’t configure it yet.

clang-format --style=Google --dump-config >.clang-format

I usually just start with Google style and adjust it as I go. The default template is ready to be committed:

git add .clang-format
git commit -m "build: add clang-format configuration"

Editors configuration

All of this integrates nicely with editors I use. I mostly use neovim but occasionally jump into emacs - no specific reasons for that really, just depends on the mood :). In nvim, I use vim-codefmt which picks up clang-format configuration automatically and applies formatting every time when saving files. In emacs, there’s clang-format plugin available as well which does the same thing.

Conventional commits

This is something that I really enjoy. The idea is that, the commit format is kind of “structured”. Thanks to that, it’s possible to generate changelogs from git commits very easily. Read through the conventional commits web page for more details.

I’ve installed commitizen as a commit-msg hook so, if the commit message doesn’t adhere to the format, it will be rejected. You can try it out yourself. If you clone my test repo, run the scripts/setup_env.sh - which installs all the discussed tools, commitizen will become part of the pre-commit hooks so committing i.e.:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
594:heimdall cppsetup 0 (master) $ echo hello >hello
595:heimdall cppsetup 0 (master) $ git add hello
596:heimdall cppsetup 0 (master +) $ git commit -m "This message is not a conventional commit message"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check for added large files..............................................Passed
clang-format.........................................(no files to check)Skipped
clang-tidy...........................................(no files to check)Skipped
commitizen check.........................................................Failed
- hook id: commitizen
- exit code: 14

commit validation: failed!
please enter a commit message in the commitizen format.
commit "": "This message is not a conventional commit message"
pattern: (?s)(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)(\(\S+\))?!?:( [^\n\r]+)((\n\n.*)|(\s*))?$

Similarly, if I’m committing from either emacs magit or vim-fugitive, the commit will be rejected.

Changelogs

With all of this in place, generation of a changelog is as easy as:

poetry run cz changelog

Just as an example, the following git history:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
commit a0bd53587f318dc0c8d27f5db1c811144e65742f (HEAD -> master, tag: v0.0.2)
Author: Tomasz Wisniewski <tomasz.wisni3wski@gmail.com>
Date:   Mon May 29 20:48:34 2023 +0100

    fix: correct values returned from foo & bar

    Values returned by these functions were incorrect.  This change
    introduces the correct values.

commit 8c571759af25923cdf801f1961148cd31b5bb6f2
Author: Tomasz Wisniewski <tomasz.wisni3wski@gmail.com>
Date:   Mon May 29 20:46:39 2023 +0100

    feat: add feature bar

    This commit introduces feature bar which further extends what `foo` has
    initially introduced.

commit 5dfb7c3e44ca8ec38c2ffdffbbfc2b636e136acc (tag: v0.0.1)
Author: Tomasz Wisniewski <tomasz.wisni3wski@gmail.com>
Date:   Mon May 29 20:44:53 2023 +0100

    feat: add feature foo

    This change adds a super important feature foo that calculates the
    correct return code.

Will result in CHANGELOG.md looking like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ cat CHANGELOG.md
## v0.0.2 (2023-05-29)

### Feat

- add feature bar

### Fix

- correct values returned from foo & bar

## v0.0.1 (2023-05-29)

### Feat

- add feature foo

Why do I like this setup?

I think that using python poetry to manage dependencies has a lot of advantages. There’s less effort required to manage the tooling. Integrating new tools is easy. It also helps to create reproducible development environments so, no special setup instructions are required for new devs working on your project.

This of course changes a lot and I’m not saying that this is best setup ever period. I’m sure that it’s gonna evolve further and possibly I’m gonna find a better approach to some of the solutions I currently use.