Makeheads101: Makefiles for Dummies
Hey guys, this is macad 101, and today I'm going to be giving you a brief introduction to make files.
Now, if you're a programmer, you're probably already familiar with make files. Or if you've ever used software on Linux or compiled software yourself, you probably know what a make file is. Um, essentially, a make file gives directives to this neat utility called make, and this utility will then be able to follow certain instructions and compile a set of source code.
Now, uh, this is really useful for large projects, especially when you want to send the project to someone else and have them, you know, be able to universally compile it with one command. Um, make is perfect for this.
So, to show you some features of make and, uh, basically the general sense of how to use it, I've actually created a couple of sample files, uh, sample source files. And I've created a basic directory structure which might emulate a small C project that someone might be working on.
Now, of course, make is really general, so it doesn't necessarily have to be for C. It could be for anything that can be compiled in bash. Um, it can, because make is really just kind of a shell script extension sort of deal. Um, but C is the general thing that it's used for—C++—um, so that's what I'm going to be using for this example.
Um, so just a little background: if you're not familiar with how compiling usually works in C or C++, what you do is you have all these C files that perhaps depend on each other. For instance, this connectivity file depends on, um, the status source right here. Um, if you have a bunch of C files like this, you usually compile them each into individual object files, and the object file has all the code for that specific, uh, source file, but it doesn't actually know about any of the other source files. It just references a certain symbol name like a function name that it doesn't know about.
And then, at the end, you have GCC compile all these object files along with some kind of, uh, source file or something, and, um, that will basically make it possible for all the files at once to resolve their dependencies and for the main executable to actually have all the stuff it needs.
Um, so we're going to be using a make file to generate object files for all these source files, and, uh, it's also going to link our main executable for us, and this is all going to take one command.
So, the first step to creating a make file is to open up your favorite editor. Mine is TextMate. Open and edit a file called makefile in your project root.
And, uh, the first thing we're going to do is tell make where all of our libs are. In this case, I've called them libs because they're basically standalone functions that, uh, we want to compile that our main dependency works on. So, I'm going to declare a new variable, and the syntax for this is libs =
and then the variable content. So, I'm going to do libs/
and then I'm going to do each path to each thing. So how about connectivity lib
, SL database
, and lib SL manager
?
And I will be showing you how to abbreviate this and all that stuff, but I want to give you a sense of all the various different options you have. Um, so by the way, I'm just separating this list with spaces because that's how bash works, and that's all we really care about.
Um, and now I'm also going to declare an include path. This is only going to really be useful for our main executable, in this case for our CLI, um, because our CLI will reference manager/manager main
and database DB
, and it has to know that that's in libs
. So, I'm going to make the include path libs/
, and of course, we don't really need to declare variables for all these things, but it certainly is nice.
Um, okay, now I'm going to make a target, and a target is essentially, uh, a set of bash instructions that make will run, uh, when that target is executed. So, I'm going to declare how about a libraries target.
Now, the convention for make is that this is actually a file name, and it will only run the target if, uh, the file with that name doesn't already exist, and it can also be a directory, which in this case I think it will be. So, um, that means that usually it's convention; if we have a libraries target, let's make libraries.
And in fact, this is where I'm going to move all of our object files when we compile our libraries. So, the next step, of course, is to generate an object file for each of our source files, and we already have a list of all our source directories, so how about we, uh, loop through that?
Now, the way you do a loop in make is the same way you do a loop in bash. And in fact, because make just executes a series of bash commands, we can just do it the way we would normally.
So, how about for dur in
and now here’s here's a little something that we wouldn't see in bash: we're referencing a make variable. So, this is the syntax for referencing a variable like libs, um, and I'll show you how to access a bash variable in a second.
Um, but we just have to escape the dollar sign in order to do that. And so, one of the important things to remember is that you need a backslash at the end of a line if you want to continue onto the next line because it runs each... by default, make will run each line in its own basic shell environment.
So, we need to put a backslash until we're done with this little for loop so it runs all as one shell script. Now, we're going to cd
to the directory, so I'm going to do cd $$dur
, and the $$
escapes the dollar sign so that it's a now a bash variable instead of a, um, a make variable or a make directive or something like that.
And we need a semicolon because as you will soon discover, um, there's no new lines. Like, if I do this backslash and then a new line right after it, what will actually happen is make will feed bash all of this code right here that we're about to write all as one line.
So, you need semicolons. But now we’re cd
ed to our source directory, and so we're going to compile everything.
So, to compile a bunch of source files as a, um, as a set of object files, we use GCC -c
and then *.c
is the files we want to compile, and, um, our include directory will actually be ../
.
Um, and the reason for this is that usually in my libraries, unless I have something like connectivity
, I'm actually going to do like for instance in connectivity
, if I wanted to reference, uh, db
, I would say database/db
.
So really I want each of these to know about the other, uh, libs through uh their directory names here. I don't want to have to type lib/
or ../
or anything like that, so I'm going to make the include directory ..
and this will compile all of our, um, our sources in that directory.
So, I'll now add another semicolon, and now the next step is to actually move all the object files which we will have just created in, for instance, in this directory. We need to move all those object files, uh, to our libraries directory, so to do this, we'll do mv *.o to libraries
.
And finally, we're going to cd -
to go back to, uh, the directory we were at before running this cd dur
, and that's important for each iteration of the loop. And one more line, and that will be done, and that's our bash for loop that will compile all of our libraries.
And now I'm going to also add another target, and this is a convention that I've seen a lot, so I usually use it, um, and this is to make a clean target.
So, the clean target will delete our libraries directory, and it will also delete our executable once I set that up. But, um, I'm going to first show you how it compiles the libraries. Make sure that all works, and then I'll move along and make it compile the actual main executable.
So if I run make right now, you can actually see that if I go to our libraries directory, it has a bunch of object files, and in fact, it has an object file for each source file in here. So it would appear that this has actually achieved what we want to achieve.
So, I'm going to now run make clean
, and this is how it works: if you do make
space and then a target name, it will run that target.
Um, and if you just run make without a target name, it'll run the first target. Um, at least that's how it's always worked for me, so that's kind of what I'm assuming.
Um, and of course, I can also type make libraries
, that has the same effect right now as running the make command.
Um, so now we're going to go ahead and make a little tool to compile our command line interface, and this is where, uh, a neat thing that makes supports called dependencies comes in.
So if I know I want this command line interface, and, in fact, I'll call it cly_exec
because I already have a directory called cly
. Um, if we know that this, uh, command line interface will depend on our libraries, we can just say libraries
here, and it will now know—bash will now, when it runs cexec
, it'll first run libraries
if the directory libraries
doesn't already exist, and then it will continue on and run the code that we put in here.
And now this is where we're going to compile our main command line interface. You can do this any number of ways. You can cd
to the command line interface directory and run make there. Maybe you will have another make file inside of there.
Um, but what I like to do, or what I'm going to do for this example since there's only one file in the command line interface directory, and it's real simple, is I'm just going to go ahead and, uh, gcc -c CLI.c
. Whoops! And then I'm going to, um, tell it about all of our libraries.
Now, one of the downsides to this is actually that GCC doesn't like wildcarding like *.o
. So instead, what we're going to do is we're going to use, um, a make file wildcard, and all you have to do is like, um, a dollar sign, and then in parentheses, wildcard libs/*.o
, and this will just create a space-separated list of all of the things that match this wildcard—in that case, all of the library object files.
And this is very helpful because, um, we can use this wildcard directory anywhere or this wildcard directive anywhere in our make file, um, just like we could any make file variable like here we use lib. The wildcard is kind of like a make function, if you will.
Um, so that's useful. That's an interesting feature of make. And now, we're going to do -I
and then our include path, which in this case we're using because we're compiling the main executable and everything, and I'll -o
and then I'll do c_exec
because that's our executable name that we claim there.
And now let's go ahead and go back and make, and as you can see, it created c_exec
, and if I actually run it, there we go, it has output. I don't really care what that is because all that this really means is that it successfully linked everything with all my libraries and all that stuff.
And now, actually, I'm going to add to my clean directory, but I'm going to add to my rm
statement. I'm going to have add_c_exec
and then I'm going to make clean.
And, uh, you can actually see that it did in fact delete everything there. So this—this is very iterative. It's very short, and it will compile. Like, I could create another couple sources in here, and it'll automatically deal with all that for me.
Um, now, I did mention earlier that there was a way to simplify this, and I think you probably already were able to figure out how to do that.
Um, all you really have to do is use a wildcard here like we did up above; we can just do libs/*.o
, and now if I make, it will actually have the same effect. I can run c_exec
, and everything works.
Um, so that is how to use a make file for a basic C project. Now, this obviously doesn't cover all the little, um, you know, fancy things you can do with make, all the little tricks and neat things, you know, but it will certainly introduce you to all the options that are available, and I think this demonstrates very well how robust make is, how simple it is to use, and how it makes your source easier to manage.
Um, so thanks for watching macad 101. Subscribe, and goodbye.