Build systems are stupid

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?

Tagged , , , , ,

14 thoughts on “Build systems are stupid

  1. alexmatto says:

    I agree completely =)

    I remember the makefiles I had to make in the past, really annoying of C and C++.
    And for complex projects you also have to know complex linkage incarnations names…

  2. Ben says:

    > Why would I have files in src if I don’t want them to be compiled?

    Git conflict files have the same extension as the original conflicting file. Globs find these and build them too. Rust and other languages with de facto/standard build systems can find sources because there is a top level file from which one can find all other files. C and C++ have no concept of “source files” at all, so there’s no way to say “oh, I see you’ve imported X, let me add X.cpp to my build graph”. The first step to getting this done is getting the ISO committees to get what a source file *is* into the standard.

    –Ben (CMake developer and SG15 Tooling member)

    • atilaneves says:

      Thanks for the comment. I know there are no files in the standard, which is why modules look like they do. Still, gcc is an implementation of it that uses files so I don’t know. Things should just work IMO.

  3. rain says:

    I remember this experiment to include the needed information to build a C project inside comments in the C source files: https://github.com/rofl0r/rcb

  4. Jibran says:

    > I also think that new languages should ship with a compiler-based build system, i.e. the compiler is the build system.

    Reminds me of the Go language. The command `go build` builds the package you specify (also maybe the package whose directly you’re currently in, not sure on this). No configuration needed to specify what files to include and what not.

  5. Napoli says:

    This is what Rust’s Cargo does. It compiles src/lib.rs as a library, src/bin.rs as the program, and finds their dependencies automatically. The manual tinkering with builds is not needed.

  6. coco says:

    You are overcomplicating things. You do not even need a makefile in your first example, you can simply run “make foo” and it will build your program.

    • atilaneves says:

      That wasn’t the point.

      • coco says:

        It was not clear to me what your point was, then. You said explicitly ” And yet, this is probably the simplest way I can come up with to tell the computer to do it properly:”, which is manifestly not true. You can tell the computer “make foo” without need to create any file, and it will solve your problem, and I doubt it can even get any simpler than that! Maybe you can clarify your point with a concrete example where a more complex solution is required and not available with common tools?

  7. atilaneves says:

    When I said “this is probably the simplest way” I was referring to the project with 3 directories. `make foo` won’t work there. Implicit rules won’t work either.

    > Maybe you can clarify your point with a concrete example

    There’s a complete example right there in the blog post.

  8. renozyx says:

    Then you have a project where you want to use a python script to generate foo sources and your language specific foo-builder doesn’t work anymore, so you build a makefile again..

  9. Joe Nelson says:

    How about this:

    “`
    $ tree

    .
    ├── Makefile
    ├── include
    │   └── foo.h
    └── src
    ├── foo.c
    └── prog.c

    $ cat Makefile

    CFLAGS = -Iinclude
    VPATH = src:include

    prog: prog.c foo.o
    foo.o: foo.c foo.h

    $ make

    cc -Iinclude -c -o foo.o src/foo.c
    cc -Iinclude src/prog.c foo.o -o prog
    “`

  10. jra says:

    The worst thing about switching to a Go was I lost 20 years of hard-won Makefile knowledge. The best thing about Go is no Makefiles.

Leave a reply to atilaneves Cancel reply