Sild - we're gonna Make it after all

July 05, 2016

…if only there were a way to automate all those compilation steps…

cc utils.c -o
cc cell.c -o
cc eval.c -o
cc builtins.c -o
cc print.c -o
cc read.c -o
cc utils.o cell.o eval.o builtins.o print.o read.o main.c -o sild

I could put this all into a shell script, maybe? Sure, but then I would be recompiling everything every single time I wanted to make a new build. Once again, not a big deal on a project of this small size, but bad practice. Luckily, in true Unix fashion, there is a tool just for this problem, and it’s called make!


Make is… well I don’t know. It’s pretty great! Also it’s a huge nightmare! It’s a very easy tool, in the sense that its basic usage is pretty easy to explain, but it’s ultimately not very simple, as it’s easy all the way to gordion knotsville. Easy is nice, but we want simple!

Anyway, I just want a way to make my build work.

Running the make command looks for a makefile in the working directory. a makefile can be named makefile, Makefile, or GNUMakefile in the case of gnu make. Usually they’re called Makefile since this appears near the beginning of a directory listing (since it is capitalized), but I hate capital letters, so I’m naming mine makefile.

When make is invoked, it looks for a makefile, and if it finds one, it executes the default target in that makefile. A make target is a rule that described how to make that target file, and generally looks like this:

target_name : dependencies
	commmands to create target

So, for a single file, the target would look pretty familiar (it has no dependencies except for its own .c file)

thing : thing.c
	cc thing.c -o thing

The default rule will implicitly be the first rule in the file and, a note about formatting, that indent under the target/dependency declaration must be a hard tabstop character, or make will get wicked confused, as it used hard tabs to signal that that line is an actual command to execute in the shell.

Let’s say thing depends on something else, some object file or another…

thing : thing.c lib.o
	cc -o thing thing.c lib.o

(I think it looks better to put the target at the beginning of the command if there are multiple dependencies…)

But where does lib.o come from? Ostensibly, it comes from some file called lib.c! For that, we’ll need another target.

thing : thing.c lib.o
	cc -o thing thing.c lib.o

lib.o: lib.c
	cc -c lib.c

Now, in order to execute thing, make will look at its dependency list and recursively execute any subcommands that it needs to to keep all of its dependencies up to date. This is the clever part of make! Let’s say you compile the thing target, and out of that you are going to get both a lib.o file and the final executable thing file. Then you update thing.c, but don’t change lib.c, which is the only thing lib.o depends on. the next time you run make, the program knows that, since lib.c is older than the thing that is being recompiled that depends on it, that it doesn’t need to be recompiled itself, and the existing object file is ok to be linked! This is very cool! For big projects this cleverness can save a massive amount of time in the compile/test/edit cycle! And its convenient even in a small project like this.


So, as I factored out the libraries from my old big sild.c file, I added a rule for each new library in my makefile. Right now, it looks like this:

sild: read.o print.o builtins.o eval.o cell.o util.o sild.c
	cc read.o print.o builtins.o eval.o cell.o util.o sild.c -o sild

util.o: util.c
	cc util.c -c

cell.o: cell.c
	cc cell.c -c

eval.o: eval.c
	cc eval.c -c

builtins.o: builtins.c
	cc builtins.c -c

print.o: print.c
	cc print.c -c

read.o: read.c
	cc read.c -c

This worked, but I don’t like it. I felt like there was something I was missing about make, something I didn’t quite get! That is awfully repetitive, not very DRY code, there must be a better way!

Make supports variables internal to itself, I can start by defining CC (for ‘C compiler) to point to my compiler of choice, which is clang, which is invoked on my machine via cc

CC = cc

Now, I replace all the cc calls in my rules with a variable expansion of that var:

CC = cc

sild: read.o print.o builtins.o eval.o cell.o util.o sild.c
	$(CC) read.o print.o builtins.o eval.o cell.o util.o sild.c -o sild

util.o: util.c
	$(CC) util.c -c

cell.o: cell.c
	$(CC) cell.c -c

eval.o: eval.c
	$(CC) eval.c -c

builtins.o: builtins.c
	$(CC) builtins.c -c

print.o: print.c
	$(CC) print.c -c

read.o: read.c
	$(CC) read.c -c

Now, if I want to change my compiler, I just change one line.

It’s useful to have a clean target to remove all the artifacts from a build. The commands to do that now would be like this:

clean:
	rm sild *.o

I’ve also added those to my .gitignore file, as I don’t need to commit any of these artifacts since they are derivable from the source code.

sild
*.o

clean is the traditional name for this task, but it presents a little bit of a problem. What if there is a file named “clean” in the working directory? Make might not execute these commands if it looks at the clean file and sees that it doesn’t need to be updated. By declaring clean as a .PHONY target, this problem is resolved.

.PHONY: clean
clean:
	rm sild *.o

There are other reasons to use .PHONY targets. Anytime you’re defining a rule that executes arbitrary commands that don’t result in an artifact, you should declare it .PHONY.

I can also add a CFLAGS variable that holds some options I want to pass to all of my compiler invocations.

CFLAGS = -Wall -Werror

These flags tell the compiler to report more errors than it normally would. It’s a good idea to do this to have extra insight into how the compiler is viewing your code.

And then in each of the rules:

...
builtins.o: builtins.c
	$(CC) $(CFLAGS) builtins.c -c
...

ONe of the benefits of modularizing everything is that you don’t have to look at all the stuff you’re not working with. I am keeping everything in a top level directory right now. This isn’t so great! It is better to hold all your source code in a src or source directory and then build artifacts outside of that directory. Here, all of my source files are living in src, and all my object files are being built to a directory called obj, which I have also added to my .gitignore.

CC = cc

sild: obj/read.o obj/print.o obj/builtins.o obj/eval.o obj/cell.o obj/util.o sild.c
	$(CC) obj/read.o obj/print.o obj/builtins.o obj/eval.o obj/cell.o obj/util.o sild.c -o sild

obj/util.o: util.c obj
	$(CC) util.c -c -o obj/util.o

obj/cell.o: cell.c obj
	$(CC) cell.c -c -o obj/cell.o

obj/eval.o: eval.c obj
	$(CC) eval.c -c -o obj/eval.o

obj/builtins.o: builtins.c obj
	$(CC) builtins.c -c -o obj/builtins.o

obj/print.o: print.c obj
	$(CC) print.c -c -o obj/print.o

obj/read.o: read.c obj
	$(CC) read.c -c -o obj/read.o

obj:
	mkdir obj

.PHONY: clean run
clean:
	rm sild
	rm -r obj

This works, but is getting pretty ugly and verbose and unmaintainable. If I want to change where I build to, for example, I have a lot of search and replacing to do. I can do better!

I did a lot of research on this one, and I had that experience where someone on stack overflow asked this exact same question and someone gave them the exact answer that you are looking for. Terrific! The original poster edited their question with their working solution, you can find it just under “Here is the working makfile:

Let’s look at what that means for my makefile!

SHELL = /bin/sh

I’ll add this as a best practice, in case the working shell is something other than bash, which is what I’m expecting.

These are the same as before:

CC = cc
CFLAGS = -Wall -Werror

Here I’ll define a var OBJDIR for use in the target rules. The vpath is telling make “look in this path for this type of file when searching for dependencies.

OBJDIR=obj
vpath %.c src

And one that represents all of the dependencies that need to be built for the main executable. Notice I’m using the addprefix make function to prepend all of these names with the OBJDIR variable:

objects = $(addprefix $(OBJDIR)/, util.o cell.o eval.o builtins.o print.o read.o main.o)

Now, the dependencies of the executable can be expanded from the var that was constructed above.

sild: $(objects)
	$(CC) $(CFLAGS) -o sild $(objects)

Much cleaner!

Here’s the beastly rule:

$(OBJDIR)/%.o: %.c $(OBJDIR)
	$(CC) -c $(CFLAGS) $< -o $@

This rule make my head hurt, but it’s basically saying “define a target filename.o for every .c file in src/”.

%.c is associated with src in the vpath assignment, and then:

$<     refers to the dependency names.
@<     refers to the target name.

The OBJDIR target needs to know how to create itself, here that is as simple as a mkdir:

$(OBJDIR):
	mkdir $(OBJDIR)

And Bob’s your uncle!

.PHONY: clean
clean:
	rm sild
	rm -r obj

I’ll be honest, learning about make was an unwelcome detour from the business of writing this interpreter. I found it pretty counterintuitive, at least until I had a handle on how to refactor out libraries like I described in the last post. make is a great tool, don’t get me wrong, but it felt pretty archaic to be registering all my source files one by one and describing the commands needed to build them individually.

This makefile is a lot better than what I started with, though, and if I want to add a new dependency to the executable I only have to add it in one place, which was really the goal! I will likely not be touching this makefile again except to do just that; I don’t plan on adding any other built executable targets to this.