Sild - we're gonna Make it after allJuly 05, 2016
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
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.
make command looks for a
makefile in the working directory. a
makefile can be named
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
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.
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
thing target, and out of that you are going to get both a
and the final executable
thing file. Then you update
thing.c, but don’t
lib.c, which is the only thing
lib.o depends on. the next time you
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
‘C compiler) to point to my compiler of choice, which is clang, which is
invoked on my machine via
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.
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
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
telling make “look in this path for this type of file when searching for
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
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)
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.
OBJDIR target needs to know how to create itself, here that is as simple
$(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
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.