What is a better make?
What exactly do I mean when I say “build a better make”? What do all those people that have tried to build a better make over the years mean? I’ve compiled a list of common complaints about make and added some of my own thoughts. I also look at some of the things that make is really good at that should be preserved.
- Make uses timestamps to determine whether a file has changed instead of something like checksums. This is imprecise and prone to error, especially across distributed file systems.
- Make is slow. Builds of typical large software systems still take hours, if not days, despite the tremendous advances in CPU and disk speeds over recent years.
- Some make implementations do not support parallel builds (on multi-processor machines and/or on multiple machines).
- Make is not easy to debug. It has very limited support for customizing its logging output, and this output is text-only and not geared towards further machine-based parsing. Makefiles are also not easy to debug; for example it is not possible to “step through” them.
- Make does not allow extracting information on the applied rules or dependency graph in any form for further processing with another tool or convenient display to a user (inside, for example, an IDE), and in general does not allow a lot of access into its “internals”.
- Make does not make it easy to understand the differences between the “core” source files, the “intermediate” products and the “final” products, nor does it make it easy to distinguish between “actual” sources and “tests”.
- Make does not understand the differences between different hardware and/or software platforms. This means that different Makefiles must be written for each platform, or in an awkward way to support building on multiple platforms.
- Make is designed for a POSIX environment, making it ill-suited for use within the windows world and ill-suited for integrating with windows (and windows-style) developer tools (note there are various ports available of the make program itself for use on windows).
- Make comes in many versions, shapes and forms which do not all support the same command-line arguments, features, or Makefile formats. There is no “standard”.
- Makefiles use an awkward mix of macro expansion and the bourne shell coupled with a rather unique (though compact) syntax for defining rules and targets, instead of a sane and easy-to-understand language.
- Makefiles use the system shell (usually the bourne shell or the bash shell), which is notorious for being incompatible across platforms and systems.
- Makefiles require the use of tabs in a specific way. Tabs are hard to distinguish from spaces when editing or reading. Also, many text editors convert between spaces and tabs automatically or the other way around.
- Makefiles do not conveniently support something such as “user-defined functions”. This leads to developers creating their own utility scripts or programs to go along with makefiles. This often reduces the portability of the build, results in “hidden dependencies”, duplication of effort, and a cluttered harder-to-understand source tree.
- Makefiles do not support the “core” or “common” programming language constructs such as for loops or if/then/else clauses. (this is also an advantage since it should lead to a ‘declarative style of programming.)
- Makefiles are not easily modularized in a way that makes them reusable across projects, platforms and implementations.
- Makefiles do not allow defining set pieces of functionality (like a user- defined function or a plugin) which are well-isolated from the rest of the file.
- Make requires manual maintenance of static definitions of dependencies, instead of determining them dynamically (like, for example, inspecting C source code for
#includestatements, java source code for
- Make does not support transitive or cascading dependencies.
- Make does not provide a way to extract the (implicit and explicit) rules relating to its build processes.
- Make does not provide a way to extract the dependency graph between build sources, products, etc.
- Make does not provide understand dependencies between multiple-file “pieces” (eg it does not know about “modules” or “projects”).
- Make its support non-file dependencies (“phony targets”) is second-class and does not work well for complex
- Make does not support distinguishing between build-time dependencies, test-time dependencies and runtime dependencies.
- Make does not support distinguishing between optional and non-optional dependencies.
- Make does not support “delegating” pieces of its file-based dependency management to other tools (like when invoking the “javac” or “gcc” commands which do dependency management on their own).
- Make does not make it easy to mix-and-match the use of precompiled binaries with the use of “make-managed” source code.
- Make does not know how to distinguish between build products and intermediate build products that make manages and the ones that are managed by other tools (or manually).
If there’s such a long list of problems with make and there are so many missing features, why is make used so much? It has a lot going for it:
- de-facto standard for C/C++/native builds in the unix/linux/posix world.
- installed on most linux/unix/posix systems by default. If not installed by default, both source and binaries usually readily available for just about every hardware and software system in existence.
- mature, stable, flexible, powerful, just about bug-free, well-tested.
- open source / free software, as well as commercial versions.
- well-documented, with man pages, info pages, generic tutorials, problem-specific tutorials, reference manuals, and many books.
- usable for C/C++, fortran, java, python, perl, LaTeX, and much much more. Many examples available online on how to do this.
- simple things are simple, many complex things are possible.
- usable for software builds but also flexible enough to use for testing, software management and distribution, with again many examples available online on how to do this.
- repeatable builds. Unless you do silly things in a Makefile, different invocations of make on the same source tree will yield identical results.
- verifiable and testable behaviour. Make its “base” behaviour is well- specified and dependent on something easily set and modified (file modification time) and hence easily tested.
- light on dependencies. To bootstrap a make build, all you need is a version of ‘sh’, a C compiler, a C library, some standard POSIX C libraries, and a linker such as ld. To build make “properly”, you additionally need autoconf, automake, and an existing ‘make’ binary. To run the make program, you need nothing but the binary (and perhaps shared libraries if using shared libraries). This almost complete lack of dependencies means make is suitable for eg bootstrapping the build of an operating system.
In all honesty, the above list is way too short. Make “works” for a vast amount of “build problems”. Its really hard to list all of the things that make is used for, usable for, or excels at: the list would be extremely long.
Attempts at a ‘better make’
There are many attempts at improving on make or creating a viable alternative. The most common approaches seem to be:
- “wrap” make into a different environment. Create helper utilities that are called from make and utilities that invoke make. Create utilities for writing Makefiles (like generating them from some higher-level language). Examples: autoconf/automake/libtool, BSD ports, portage/emerge.
- create a make alternative using a specific programming language. For example, create a make-like tool optimized for working with the language the tool is written in, or create a tool targetting a specific platform (such as python, java or .Net). Examples: python distutils, ant, NAnt, MSBuild, rake.
- create a “full” “from-scratch” replacement which does not use or require make yet is designed to work for similar things. Examples: cook, scons. (Whether a tool belongs in category 2 or category 3 is probably a good topic for a heated debate without a solid possible conclusion — most of the tools I put in category 2 are perfectly usable for most if not all of the same stuff that the ones in category 3 are usable for.)
- create a higher-level tool which is not intended as a replacement, but among other things also solves the problems that make solves. Examples: most IDEs (Integrated Development Environments) such as Delphi, maven, Visual Studio .Net, many “CASE” tools. (Often, these tools or environments build on top of a tool which is much like a make replacement, e.g. maven uses a lot of ant under the covers, Visual Studio builds on MSBuild)
Each of these approaches comes with some advantages and some disadvantages. This means that ‘better’ or ‘worse’ is often a lot harder to define. For example, for building Ruby software, the ‘rake’ tool is quite obviously a lot ‘better’ than ‘make’, and if you are a Ruby programmer and like the “ruby way” of working, chances are rake is also a better choice for you when building native things.
Similarly, there are not a lot of java developers who will ever seriously consider using make for managing their java-based projects. It makes much more sense to them to use a tool written in java, extensible in java, and integrating well with their java-based development environments.
I find the ‘cook‘ tool to be very interesting since this kind of non- comparability does not apply to it. Its written in C, just like make. Cook is also a very old and very mature tool which was first developed when most of the software in the world was still very much ‘native’ and hence is also a very direct replacement for make, yet it is much more radically different from make than many tools written for similar goals during the same time. I think cook doesn’t see much use these days, at least not when compared to make. Maybe it suffered from the same problem as smalltalk – while being very good and very useful, it was just “too different” and “came too early” for the world to be “ready” for it.
My definition of a “better” make
“A better make”
- suffers from fewer of the limitations/problems that I described above (preferably none of them of course).
- preserves all of the useful features of make that I described above (preferably also quite a few I didn’t describe).
Okay, that’s an interesting definition but its missing some kind of “functional testcase”. Lets add a few real-life tests. “A better make” is a good basis for the build systems of projects like
- GNU Bash
- GNU GCC
- GNU Classpath
- Apache HTTPD
- Apache Cocoon
- Mozilla Firefox
I think that if you built a tool to reduce the “build pain” for all of these projects that you could then really lay claim to the “a better make” title.