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 runcmake
. - 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.
❤️❤️❤️