Making Makefiles25th March 2019

Recently I had to set up a cross-compile build for a project that had to run on Windows (32 and 64 bit) and Linux. The process included getting a couple of dependencies, configuring and compiling with rather long commands (at least on Windows), and then copying all the things into the right places and bundling it all up in an installer. This quickly turned into a rather long README file documenting all the different steps that needed to be taken and the process of following it was so error prone that even I who wrote the instructions managed to miss a step on multiple occasions. This was obviously not great, and besides, who wants to manually build stuff! So I decided to automate the process, and the tool of choice was the classic make.

Makefiles have always been a bit cryptic to me, I've written and modified them but never really taken the time to actually understand them. But after a talk at my local university that gave some insight into what the tool was actually meant to do I realised it would be a nice fit for this task. You see make, in it's simplest form, is just a list of recipes for getting files. The way it works is that if the Makefile, or any of the dependencies for a given file is younger than the target file (or missing in the case of dependencies), then make will re-run the recipe to get the file again. So the source code dependencies could be a simple recipe with wget and unpacking of a zip file, and my build steps could depend on the created files. Then my step to create an installer could depend on those steps and running make would consider the entire project and rebuild only the necessary parts to properly build the finished product. But as with all things that seem simple there were some gotchas, which is why I'm writing this scrap to remind myself of those gotchas for future makefiles.

First off every command in a makefile runs independent on any other command, this means that cd won't work like you expect. There are flags to change this behaviour, but I choose to not use those and stick with the more classic approach.

Second of is depending on directories. A recipe has no issue with being the target for a directory, but whenever a file within a directory is updated the timestamp of the directory is updated. This means that my initial approach of having one target grab a zip file and unpack it into a directory, and then having another target depend on this directory and configure and make the project within that directory would always trigger a recompilation since the directory would be the same age as the compiled file (or younger). The way I solved this was to simply depend on a file inside the directory that shouldn't change when the compilation is run. In theory it would be possible to have more complex rules for this, even something as simple as putting the initial content of the directory (potentially with a blacklist) in a file and depend on all those files would be more robust than what I did.

The last note I want to make (this scrap is already dragging on) is that recipes can be re-run without the files missing. Since it's all based on timestamps it is possible that files are still left on the system when the recipe is run. It's always a good idea to offer a clean command, potentially also one that only cleans intermediary build files and not the result, and while these can be used to purge bad state lying around it mostly defeats the purpose of make.