Computer Mad Science: The Anatomy Of A Makefile Part 1
Posted 01-28-2012 at 01:17 PM by rainbowsally
Features:
Requires:
This might be interesting for even non-programmers, just to see how makefiles efficiently work through their dependencies.
[If you're already up to speed on what make does, skip to Part 2 and we'll create a makefile creator that you might just find is perfect for smallish projects that you'd rather not clutter up with a bazillion unnecessary files and sluggish, redundant "configure" runs.]
First let's create an empty makefile named Makefile. It's just a text file.
And run 'make' in that folder with the Makefile.
Here's what we get.
No big surprises there, but we see that 'make' expects some "targets".
Let's now add "all:" to the makefile and see what happens. (Don't miss that colon after the name.)
[If you get any "missing separator" errors in any of these things, it's almost certainly a missing colon or a missing TAB. In the rules section, which we'll talk about in a sec, the spaces before the commands must be real TAB characters.]
And...
We've seen that before in 'make' builds, especially when make is descending into directories containing non-code stuff in a system of files created with "automake".
And again, this is not too surprising. But we notice that apparently "all:" is in fact a "target".
Change the name of "all" to "target" (don't delete the trailing colon) and we get the same notice for the new name, though 'all' is by convention used for the first target which is the start of the 'rules' section and below this first line assignments such as we see at the top of normal makefiles are not allowed.
And what we see if we look at a typical makefile is that there are tons of assignments (this equals that) and that environment variable names are enclosed in parens. So $HOME as written in bash needs to be written $(HOME) in a makefile.
Other than that all the stuff above the rules section appears to be somewhat more forgiving than bash. For example spaces between names and "=" signs and we don't need to quote strings composed of several words.
So there are two 'sections' in the makefiles. The assignments section and the rules section.
So now let's start assigning and creating the "rules" section. And by 'rules' we mean simply that we have to perform a target's "depenecies" before we perform the function defined in the target's commands (if it has any).
And here's what we get.
Notice that in addition to echoing the line it also shows the command itself. Since the string contains unusual spaces in it, we can see that anything in the assignments section of the makefile is taken literally as if in quotes.
If you add the apostrophe in "I'm", however, you will indeed have to put it in quotes because make also recognizes quoted string with single and double quotes.
Now let's add some dependencies for our target and see what happens.
I know you can't stand the suspense, but what that does, you'll have to find out by doing it. :-)
What do you think is going on here?
What do the "@" signs do?
How did 'target' know echo's name? :-)
In what order were the various commands executed.
What would happen if you re-organized that so that target's only dependency was dep3 and dep3's only dependency was dep2, and dep2's only dependency was dep1?
Here's that makefile and we'll use a "#" comment which works the same as a bash comment in either section, to tell what we're doing for people who want to do development on this project.
Now let's get serious. A real makefile usually (always?) works with files.
Let's write the above dialog to a file.
Type 'make' several times and take a look at temp.txt (How many times did the same text get written to the file? Lots.)
Type 'make clean' to delete the file.
Now change the make file to this and run it several times and then look at temp.txt
What do you think is going on this time?
Where did the dependecy name "temp.txt" come from?
[It's the file name itself. The name could have included the path as well.]
Why didn't the make file keep writing strings into the file this time?
[Once the file existed there was nothing more that needed to be done for the
'rule' governing temp.txt.]
And that's the basic anatomy of a makefile.
We'll look at how and when a file may be re-created depending on its own dependencies (other files) a bit later. But before we sign off for this session let's try one more experiment.
Keep the line with "all" on it but remove the "echo" line below that.
Type 'make clean'
And then 'make'
No surprises there. Okay, now what do you think will happen if you type 'make' a secondor third time after you 'make clean'?
Starting to look familiar?
Any number of files can be listed on the 'all' line and any tag/target can have any number of dependecies. If they end up recursing, make will warn you.
Generally, *ALL* headers should be considered dependencies of *ANY* C/C++ file in a buildbecause changes in one header can change how a C/C++ file has to deal with other C/C++ files.
The principle of conditional compilation (similar to that last experiment) will allow us to conditionally compile only sources that change unless ANY header does, in which case the only reliable way to rebuild is to rebuild the whole thing.
Conditional or 'incremental' building leads to very fast edit-compile-test cycles which is extremely nice for large projects.
Are you tooled up for C/C++ yet?
We might just do some Computer Rocket Mad Science soon. The cutting edge, after all, is attached to a preferably blunt-ish handle.
:-)
- Anatomy of a Makefile
Requires:
- an editor that can produce real TABs
- make
- And bash (with a 'B')... Not DASH, the Debian default shell, which can cause problems with some makefiles, especially on KDE sytems when trying to link
libpthreads.
This might be interesting for even non-programmers, just to see how makefiles efficiently work through their dependencies.
[If you're already up to speed on what make does, skip to Part 2 and we'll create a makefile creator that you might just find is perfect for smallish projects that you'd rather not clutter up with a bazillion unnecessary files and sluggish, redundant "configure" runs.]
First let's create an empty makefile named Makefile. It's just a text file.
And run 'make' in that folder with the Makefile.
Here's what we get.
Code:
$> make make: *** No targets. Stop.
Let's now add "all:" to the makefile and see what happens. (Don't miss that colon after the name.)
[If you get any "missing separator" errors in any of these things, it's almost certainly a missing colon or a missing TAB. In the rules section, which we'll talk about in a sec, the spaces before the commands must be real TAB characters.]
And...
Code:
$> make make: Nothing to be done for `all'.
And again, this is not too surprising. But we notice that apparently "all:" is in fact a "target".
Change the name of "all" to "target" (don't delete the trailing colon) and we get the same notice for the new name, though 'all' is by convention used for the first target which is the start of the 'rules' section and below this first line assignments such as we see at the top of normal makefiles are not allowed.
And what we see if we look at a typical makefile is that there are tons of assignments (this equals that) and that environment variable names are enclosed in parens. So $HOME as written in bash needs to be written $(HOME) in a makefile.
Other than that all the stuff above the rules section appears to be somewhat more forgiving than bash. For example spaces between names and "=" signs and we don't need to quote strings composed of several words.
So there are two 'sections' in the makefiles. The assignments section and the rules section.
So now let's start assigning and creating the "rules" section. And by 'rules' we mean simply that we have to perform a target's "depenecies" before we perform the function defined in the target's commands (if it has any).
Code:
string = Im doing something . all: target target: echo $(string)
Code:
$> make echo Im doing something . Im doing something .
If you add the apostrophe in "I'm", however, you will indeed have to put it in quotes because make also recognizes quoted string with single and double quotes.
Now let's add some dependencies for our target and see what happens.
Code:
string = "I'm doing something!" all: target target: dep1 dep2 dep3 echo $(string) @echo "That's all I can tell you right now." dep1: @echo "My name is 'echo'" dep2: @printf "What are you doing? " dep3: @echo "if you don't mind my asking." @echo
What do you think is going on here?
What do the "@" signs do?
How did 'target' know echo's name? :-)
In what order were the various commands executed.
What would happen if you re-organized that so that target's only dependency was dep3 and dep3's only dependency was dep2, and dep2's only dependency was dep1?
Here's that makefile and we'll use a "#" comment which works the same as a bash comment in either section, to tell what we're doing for people who want to do development on this project.
Code:
string = "I'm doing something!" all: target target: dep3 echo $(string) @echo "That's all I can tell you right now." # and let's mix 'em up a bit... dep1: @echo "My name is 'echo'" dep3:dep2 @echo "if you don't mind my asking." @echo dep2:dep1 @printf "What are you doing? "
Let's write the above dialog to a file.
Code:
string = "I'm doing something!" all: temp.txt temp.txt: target @echo "temp.txt has been created." target: dep3 echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt # and let's mix 'em up a bit... dep1: @echo "My name is 'echo'" >> temp.txt dep3:dep2 @echo "if you don't mind my asking." >> temp.txt @echo >> temp.txt dep2:dep1 @printf "What are you doing? " >> temp.txt # this tag/target will run when we type 'make clean' clean: @rm temp.txt
Type 'make clean' to delete the file.
Now change the make file to this and run it several times and then look at temp.txt
Code:
string = "I'm doing something!" all: temp.txt @echo "temp.txt has been created." temp.txt: @echo "My name is 'echo'" >> temp.txt @printf "What are you doing? " >> temp.txt @echo "if you don't mind my asking." >> temp.txt @echo >> temp.txt @echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt clean: @rm temp.txt
Where did the dependecy name "temp.txt" come from?
[It's the file name itself. The name could have included the path as well.]
Why didn't the make file keep writing strings into the file this time?
[Once the file existed there was nothing more that needed to be done for the
'rule' governing temp.txt.]
And that's the basic anatomy of a makefile.
We'll look at how and when a file may be re-created depending on its own dependencies (other files) a bit later. But before we sign off for this session let's try one more experiment.
Keep the line with "all" on it but remove the "echo" line below that.
Type 'make clean'
And then 'make'
No surprises there. Okay, now what do you think will happen if you type 'make' a secondor third time after you 'make clean'?
Starting to look familiar?
Any number of files can be listed on the 'all' line and any tag/target can have any number of dependecies. If they end up recursing, make will warn you.
Generally, *ALL* headers should be considered dependencies of *ANY* C/C++ file in a buildbecause changes in one header can change how a C/C++ file has to deal with other C/C++ files.
The principle of conditional compilation (similar to that last experiment) will allow us to conditionally compile only sources that change unless ANY header does, in which case the only reliable way to rebuild is to rebuild the whole thing.
Conditional or 'incremental' building leads to very fast edit-compile-test cycles which is extremely nice for large projects.
Are you tooled up for C/C++ yet?
We might just do some Computer Rocket Mad Science soon. The cutting edge, after all, is attached to a preferably blunt-ish handle.
:-)
Total Comments 7
Comments
-
I am still confused with the second last Makefile and the last one: Why the second last keeps writing to temp.txt but the last one does not?
Could you please explain it more explicitly? Thanks a lot!Posted 01-07-2013 at 11:42 PM by yifangt
Updated 01-07-2013 at 11:43 PM by yifangt (typo) -
Quote:
If you're interested in how makefiles work, this probably wasn't the best example.
The interesting stuff 'make' does is in it's reordering of its procedures based on what comes after the colons in the targets. It's freaking packed with features, most of which nobody uses, but in any case, I can't remember what I was up to with this one.
If this is really a big deal to you, post here again and I'll dig into this and try to figure out what I was up to and what might have been confusing to you.
Might be that the second one is outputting to stderr instead of stdout? (first guess).
Unfortunately my modem lives in windows and my linux lives in linux so this requires a bit more than just switching to a terminal and testing the code. Hope to correct this problem soon.
:-)Posted 01-13-2013 at 07:11 PM by rainbowsally
Updated 01-13-2013 at 07:15 PM by rainbowsally -
My confusion is that:
2nd last Makefile (Makefile-b, I am aware the name must be Makefile to work!):
Code:string = "I'm doing something!" all: temp.txt temp.txt: target @echo "temp.txt has been created." target: dep3 echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt ......
The last Makefile (Makefile-a) does not.
Code:string = "I'm doing something!" all: temp.txt @echo "temp.txt has been created." temp.txt: @echo "My name is 'echo'" >> temp.txt @printf "What are you doing? " >> temp.txt @echo "if you don't mind my asking." >> temp.txt @echo >> temp.txt @echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt
1) Because Makefile-b use target as "intermediate output"?
2) Or any other reason I missed?
Thanks a lot!Posted 01-13-2013 at 07:48 PM by yifangt -
Quote:
Code:make -f makefile-b
This was a really great observation, yifangt. And I think your examples are probably better than mine because it shows how real targets (as opposed to "phoney" ones) are also file names.
See the notes in the makefiles below.
file: a/Makefile
purpose: source file
Code:# keeps writing to temp.txt if run "make" multiple times. ######################################################### # 'all' has a dependency on 'temp.txt' and the file exists # but temp.txt has additional dependenices on 'target' # so 'make' checks to see if 'target' (the file) also # exists, which it can't, because 'target' itself is a # phoney target -- never producing a file by that name. # It will always run... unless... ;-) ######################################################### string = "I'm doing something!" all: temp.txt temp.txt: target @echo "temp.txt has been created." target: # dep3 echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt
file: b/Makefile
purpose: source file
Code:# does not keeps writing to temp.txt if run "make" multiple times. ################################################################## # because the file 'temp.txt' exists. Being the only dependency # for all (either direcly or indirectly), make sees that the file # exists (and all the dependencies are satisfied) so once created, # make is done. ################################################################## string = "I'm doing something!" all: temp.txt @echo "temp.txt has been created." temp.txt: @echo "My name is 'echo'" >> temp.txt @printf "What are you doing? " >> temp.txt @echo "if you don't mind my asking." >> temp.txt @echo >> temp.txt @echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt
Try example 'a' above again and remove temp.txt and create a file named 'target' and see if it even creates the first copy of temp.txt. It can't. Because 'all' doesn't produce a file, it expects 'target' to do that.
Then remove the file named 'target' and try it again. Now it creates 'temp.txt' and keeps on writing to it for the reason that the file 'target' never exists.
Bottom line: targets are also file names. In fact 'make' also checks creation times to make sure that depenencies and the files that depend on them are 'current'.
We could attempt to demonstrate that too, but... well... Maybe later. ;-)
And thank you for your observations and your question.
:-)Posted 01-13-2013 at 09:12 PM by rainbowsally
Updated 01-13-2013 at 09:36 PM by rainbowsally (copy/paste TABs into file) -
Still writing!
Thanks!
I tried your makefile-a. First removed temp.txt, and created an empty file named "target" by:Code:rm temp.txt; touch target
Code:make -f makefile-a
I tried with another makefile:
Code:# does not write to temp.txt if file "target" exists. ############################################ # string = "I'm doing something!" # all: test_target test_target: echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt
Code:make -f makefile-c
But after I create an empty file "test_target":
Code:touch test_target; make -f makefile-c make: Nothing to be done for `all'.
Posted 01-14-2013 at 10:21 AM by yifangt
Updated 01-14-2013 at 10:41 AM by yifangt -
Still writing!
Quote:
A : B C
action
Consider the above.
Read A "needs B and C" before "action".
B and C must exist as targets in the makefile but they don't need to produce a file.
B might be a phony target, like "clean", if you like rebuilding from scratch every time. ;-)
'clean:' and 'distclean:' are phony targets. They don't create files so they will always run.
An C might be a filename, or even a library name. If it's a library name, the "action" for C might very well be to cd into another directory and 'make' there.
And the 'all' in that other makefile is no different from any of the other targets.
In fact the first target doesn't even really even need to be named "all". It's just named that by convention.
What would happen if a file named 'all' existed in the same directory with the makefile?
:-)Posted 01-17-2013 at 04:08 AM by rainbowsally -
Hi again. :-)
About the "still writing" problem. I think there might be something going on with the way linux caches files or your browser might not have refreshed. But try this -- exactly -- and it should behave as expected.
Create the file below in a new directory named 'a'.
file: a/Makefile
purpose: source file
Code:# keeps writing to temp.txt if run "make" multiple times. string = "I'm doing something!" all: temp.txt temp.txt: target @echo "temp.txt has been created." target: # dep3 echo $(string) >> temp.txt @echo "That's all I can tell you right now." >> temp.txt
Code:echo > terminal
No "temp.txt".
It SAYS it created it, but that's because temp.txt (the target) doesn't make the file in this case. "target" (the target) does, but since the file 'target' was up to date, the target named "target" never ran.
Clear as mud, eh? ;-)Posted 01-17-2013 at 05:01 AM by rainbowsally