Make it Simple

A build system need not be complicated

Preface

I'd like the chance to preface this article by addressing my long absence from writing. For the sake of brevity, I'll state simply that work, family life, and hobbies have taken up nearly all of my time over these last few months. Furthermore, this September, we will be welcoming another addition to our family - a baby girl. While my wife and are are tremendously elated to, it has called to mind all of the work that has been left untended to over the past several months. One of those tasks has been keeping up with my writing and how thoroughly remiss I would be if I were to go throughout the remainder of the year without publishing another post.

Therefor, without further adieu, allow me to talk about one of my favorite tools to use in software: Makefiles.

The Modern Build Tool Landscape

In the modern era of software, we are incredibly fortunate to have a wealth of tools at our disposal. There are myriad programming languages, compilers for those languages in various operating systems, open source libraries, command line tools, etc. While it is incredibly humbling to live in such an age of widespread proliferation of tools to use when solving programming problems, I can't help but wonder at what we may be losing. Wealth and success do not come without their own tradeoffs.

To be more precise, my thoughts have lingered not just around the exponential growth in available programming languages we've witnessed, but the systems we use to organize the code written in these languages. These days, many new languages will come with a custom build system and maybe even a package manager for developers to leverage.

  1. Rust has Cargo
  2. Go and Zig have their own integrated tool suites (not just compilers)
  3. Java has Maven, Ant, and Gradle (perhaps many more)
  4. Scala has SBT
  5. C++ now has CMake

And the list goes on …

Now normally, this is something to be praised. Developers can work with the language(s) of their choice and run a few simple commands and whatever build system they have adopted will take care of the rest (this especially holds true for newer languages and build tools). For example, in a few short commands, a Rust developer can immediately generate a new project, build, run, and test it:

cargo new my_project
cd my_project
cargo build
cargo run
cargo test

So what's the catch?

Over reliance on a simple process and a lack of understanding on how a build works under the hood. Allow me to explain.

As someone with a background in C/C++, I've had to arduously learn and relearn how compilation works for a program before some semblance of the actual process stuck. It looks something like the following:

  1. Preprocessing: This is where directives like #include are handled and #defined macros are expanded. Your source code is essentially transformed into a final product.
  2. Compilation: Your preprocessed source code is checked for syntax and semantics before being compiled down into assembly language.
  3. Assembly: The Assembler takes the produced assembly code and translates it down further int hardware / architecture specific machine code (commonly referred to as object files)
  4. Linking: The linker will take all of the assembled object files to produce an executable.

While the above process is rather unchanging, the organization of a project has historically been left up to the user, although industry standards and conventions have been leveraged for the sake of clarity. For example, I have adopted the following convention for organizing ny projects:

- include/ # A directory to store header or include files
- src/     # A directory to store source or implementation files
- obj/     # A directory to store generated intermediary files, like object files
- bin/     # A directory to store generated binaries
- doc/     # A directory to store both manual and generated documentation

Now traditionally, most software engineers understood the above processes and conventions very well in order to get their programs off the ground. But the ubiquitous desire for simplicity in building software has led to a market saturated with language specific build tools that have entirely obfuscated the underlying details.

Again, its understandable as to why we've gone this route. Human beings crave efficiency. But in doing so, I'm convinced were losing the ability to understand how a particular software build is constructed and works. In short, the overabundance of efficient and precise tooling is blinding us to the big picture, making us more dependent on the tool itself and stripping out knowledge and understanding.

Why GNU Make

So why use GNU Make

Use Cases

C Project Example

Emacs Framework Example

Final Thoughts