Big complicated projects nearly always need a build system. They might be generating code. They might be compiling different branches depending on the platform or a build-time configuration option. They might turn on/off unity builds for C++. For most simple projects however, build systems are stupid.
Let’s say you have a single C file. We need to tell the compiler the name of the file we want to compile, and the name of the resulting binary unless we’re happy with it being called a.out
, which is unlikely:
gcc -Wall -Wextra -Werror -o foo foo.c
Maybe it’s the first commit in brand new repository and all you have is foo.c
in there. Why am I telling the compiler what to build? What else would it build??
$ ls foo.c $ gcc gcc: fatal error: no input files compilation terminated.
“Real projects don’t look like that!”. Ok:
$ ls build include src $ ls src foo.c main.c $ ls include foo.h
I doubt any humans who have ever written C before would be confused as to how this project is supposed to be compiled. And yet, this is probably the simplest way I can come up with to tell the computer to do it properly:
CFLAGS ?= -Iinclude -Wall -Wextra -Werror -g SRCS := $(shell find src -name *.c) HDRS := $(shell find include -name *.h) OBJS := $(subst src,build,$(subst .c,.o,$(SRCS))) build/foo: $(OBJS) gcc -o $@ $^ build/%.o: src/%.c $(HDRS) gcc $(CFLAGS) -o $@ -c $<
Is there a better way than $(subst)
? Probably. I don’t care. I was also too lazy to figure out dependencies so all source files depend on all headers. That’s a lot of lines of Makefile and all I wanted to specify were that source files are in src
, headers are in include
, binaries go inbuild
, and that the compiler flags are -Wall -Wextra -Werror -g
. The first three are obvious from the existence and names of those directories, so there are 8 lines of code here to tell a computer what the compiler flags are. Robots: 1 Humans: 0.
CMake isn’t that much better:
cmake_minimum_required (VERSION 2.8.11) project (foo) file(GLOB SRC_FILES ${PROJECT_SOURCE_DIR}/src/*.c) add_executable(foo ${SRC_FILES}) target_include_directories(foo PRIVATE ${PROJECT_SOURCE_DIR}/include)
Robots: 2 Humans: 0.
I’ll probably get told off for using file(GLOB)
because what I really should be doing is manually listing source files because reasons. Why would I have files in src
if I don’t want them to be compiled? In what universe does it make sense for a person whose job it is to tell computers what to do so that they don’t have to do it themselves to tell a build system which files to build? In my opinion every new language should default to doing what Pony does. Running the compiler just builds everything in the current directory tree.
I also think that new languages should ship with a compiler-based build system, i.e. the compiler is the build system. The reason for that is that as a developer, I want to rebuild the minimum possible amount of code that is required after my edits to run my tests. The CMake solution above fails at this: the compiler calculates dependencies and automatically feeds it into the build system, but those dependencies are too coarse.
If I touch any header, all source files that #include it will be recompiled. This happens even if it’s an additive change, which by definition can’t affect any existing source files. If I change a function’s signature that is only used in foo.c, well tough: I’ll have to wait until bar.c, baz.c and quux.c all get recompiled for the temerity of depending on different functions from the same header.
This problem is bad enough in C and C++ but actually gets worse for languages with modules: now every file is a “header”. Changes to the implementation trigger recompilations, because our build systems are too stupid to know any better. I can’t see how a good build system can be an external tool unless the compiler is a library, and even then it would be doing the same work twice since dependency calculations are better done while a module/translation unit is being compiled.
Taking it a step further, the whole concept of caring about files and modules is probably outdated anyway. Why not a Smalltalk-like environment? You make changes to the source and your “image” gets rebuilt in the background, one part of the AST at a time, automagically resolving and rebuilding dependencies (at the function level!).
Computers and build systems: what have you done for me lately?