How To Automatically Build, Compile, and Test CMake/ C++ Unit Tests on File Save

It’s alright to spoil yourself as a developer. I mean it. Make it easy on yourself. Case in point: limit the amount of things you need to do in the development lifecycle.

In this article I describe how to set up a C++/CMake workflow that will automatically build, compile, and test your program when you hit SAVE.

This will be the final result:

It isn’t shown, but I’m saving the file (via vim) with escape + :w . When I do that the right side starts sputtering.

Making It Easy

I remember the first time I saw the automatic building of React apps. I thought it was cheating. I would make a change, hit save, and immediately see it in the browser. What’s more, I could write unit tests, save the file, and immediately see if the test pass.

So imagine the 💡 moment when I started development work on a C++ project. In college, of course, I knew nothing of build systems. back then CMake wasn’t even a thing, and Docker was a couple years away, so anything had to be done the old fashion way: gcc program.c -o program. Thankfully I was a Linux user, but I’m sure the windows students go more frustrated.

Enough of the Wild Wild West days of g++ 🤠. Let’s get to the fun automation part! 😝

For The Impatient

There’s a repo with this example that can be used as boilerplate code: https://github.com/src-r-r/cmake-automation-example

Mounting the Volumes

My first order of business was to get a streamlined build going.

I opted for the danger89/cmake docker image, since it already had CMake and seemed pretty stable.

Next, I needed a way to build my project within docker.

This can’t be done in a Dockerfile. If you copy your project with COPY , it’s will be copied into the docker container and be unchanged. There needs to be a way to update the code within the docker container when the host changes it.

So, instead, the project must be mounted as a volume:

$ docker run -v ./my_project/my_project danger89/cmake

Or, since I’m lazy, within a compose file:

We Need a Custom Dockerfile

Just a small one…the base danger89/cmake build doesn’t have entr installed. If you don’t want to use a dockerfile, include some code to check if it’s installed in the init.sh script (shown below) before running it.

Automating the Build & Test

Now we get to the fun part: running a build (and test) on a file save.

As long as we’re editing anything within the ./my_project directory, this is possible.

If you’re using node, you’ve probably crossed paths with a tool called nodemon, which watches a filesystem for changes, then runs a command when any watched file changes.

I started off thinking this was the ticket. However, I soon found it wasn’t.

In order to start an automated build I needed to do the following:

  • Watch CMakeLists.txt for changes, then run cmake .
  • Watch anything under src for changes, then run make and the test executable.

Nodemon watches path extensions, so I’d need to watch for txt , and provide a command for that, then watch for cpp, and provide a command for that, then watch for hpp and provide a command for that. Too many loose ends, and not enough granularity.

Enter Entr

Instead, there’s a Linux utility called entr that allows you to watch for filesystem changes and run a command.

So to watch for changes for CMakeLists.txt, and any src files, run the following:

$ ls ${PROJ_ROOT}/CMakeLists.txt ${PROJ_ROOT}/src/* | entr -d "/init.sh"

Where ${PROJ_ROOT} is (of course) the root of the project.

What’s in Init?

Within init.sh we’ll be running cmake, make, and then running the tests.

Now, some of you “experts” are probably gonna rail on me for both building and compiling with both the build & compile step. True, if we were to do it “correctly” we’d watch for CMakeLists.txt and execute cmake , then watch for src (in parallel) and run make.

But getting these parallel processes to work is a nightmare. So for now this is good enough.

Well…almost…

Exit on Error

Right now this script will run cmake, make, and test , unapologetically doing what its told. Is this what we want? No, we want to exit on error.

We do this by setting set -e at the top of the file:

That way if any of these commands fail, then the script will fail.

And don’t worry about enter …it’ll continue to watch the files and run /init.sh again if anything changes.

Tidying Things Up 🎁

So far we’ll be seeing A LOT of input.

We’re only concerned when things go well…so why not just show us the stage when things go wrong?

If you’ve used the terminal you’ve undoubtedly used clear to clear the terminal, or reset , which resets the terminal session.

In BASH scripting there’s another lesser-known clearing protocol that does this a lot more cleanly: printf "\033c" . It basically does a hard clear of the terminal screen.

So, let’s incorporate it into our init script:

And we’re totally fine doing this…because, remember, we’re using set -e , so if cmake fails, for instance, the BASH script terminates right there, so the printf line isn’t touched.

Integrating Into Docker

Now, where do we put these scripts?

The init script above I put in init.sh , which is mounted on an /init.sh docker volume.

The command for enter I placed in a script called watch.sh , which mounts to /watch.sh in docker.

So then the compose file with these files mounted will look like this:

Make sure these files both have executable permissions on the host.

Now, we can easily start up the my_project container and immediately start watching the files (remember, we already ran docker-compose build ).

If you didn’t run docker-compose build, don’t fret — you’ll get the same result…it’ll just take longer.

So, to get a final run-it-and-forget-about-it setup we need to execute the watch script as an entrypoint.

Now we can run docker compose:

$ docker-compose up

And now, just like magic, the project builds, compiles, and runs unit tests anytime we save a file!

Using Without Docker

If for some reason you don’t want to use docker, or you want to take something out of docker to do debugging “offline,” the script will pretty much work the same.

All you need to do is change some of the paths in watch.sh and init.sh . One thing I typically like to do is get the current directory of the script and add it to the top of the BASH script (thanks to the legend Dave Dopson at StackOverflow):

Make sure PROJ_DIR is assigned ${DIR} in the init.sh and watch.sh scripts (ensuring the above snippet is in BOTH) and you should be golden.

Now run ./watch.sh and (if you installed everything), the project should automate the build.

In this article I detailed how to set up a system to build, compile, and run a C++/CMake project automatically whenever you save a file.

As stated above, the example code is available here: https://github.com/src-r-r/cmake-automation-example. Feel free to use it in your own project

If you liked this article, please share with a few of your friends. Comment on what you found interesting, frustrating, or bizarre.

❤️❤️❤️

If you’d like to see more articles (and get the articles 2 days early!) then consider becoming a supporter on ko-fi:

https://ko-fi.com/damngood/tiers.

❤️❤️❤️

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store