Zerø Wind Jamie Wong

Make your life easier with GNU Make

I’ve been trying to keep at least one post per month going, and I was sick with stomach flu today, so here goes:

As our CS assignments get more and more complex, it’s starting to become time consuming to build, test, and submit each assignment. I could either do this every time I want to test:

java cs241.binasm < a2p8.asm > a2p8.mips
java mips.array a2p8.mips < a2p8.sample.in
java mips.array a2p8.mips < a2p8.onenode.in

Or I could just run make.

What is make?

GNU Make is a system for constructing executables or other non-source code files from source code. It allows you to specify dependencies for files and define rules for constructing them from source. For example, the rule to build a2p8.mips would look like this:

a2p8.mips: a2p8.asm
    java cs241.binasm < a2p8.asm > a2p8.mips

This says, to make a2p8.mips, run java cs241.binasm < a2p.asm > a2p8.mips. To use make, stick your ruleset in a file called Makefile in the same directory as your source then run make. This command will only run if a2p8.asm has changed since the last time it was run. These dependencies stack as well. For instance, say I need to join two files together before I compile them. I can define a ruleset like this:

linked.mips: linked.asm
    java cs241.binasm < linked.asm > linked.mips

linked.asm: a2p7.asm print.asm
    cat a2p7.asm print.asm > linked.asm

This will link the files together before it tries to compile them. It’ll also make sure it’s up to date whenever you change either a2p7.asm or print.asm. The build rule at the top of the Makefile is called the default rule. This is the one that gets run when you just run make at the command line. You can also pass a parameter to tell make to construct something specific. For instance, I could run make linked.asm here to just build the linked file without actually compiling it to machine code.

Automatic Variables in Make

One of the principles of writing good code is DRY: Don’t Repeat Yourself. Make provides various ways to support this through the use of automatic variables. Automatic variables are ones that are set for you with useful values. For instance, the following rule:


linked.asm: a2p7.asm print.asm
    cat a2p7.asm print.asm > linked.asm

can be reduced to this:

linked.asm: a2p7.asm print.asm
    cat $^ > $@

The variable $^ is a space separated list of all the dependencies. $@ is the target. See GNU make - Automatic Variables for the full list and description of such variables.

Writing Implicit Rules

Another unnecessary piece of code duplication exists for compiling multiple files of the same type. For instance, the following is repetitive:

a2p8.mips: a2p8.asm
    java cs241.binasm < $< > $@

a2p7.mips: a2p7.asm
    java cs241.binasm < $< > $@

Instead, I can define my own implicit rule for building all *.mips files like so:

%.mips: %.asm
    java cs241.binasm < $*.asm > $*.mips

Testing Using Make

One of the things I took away from my co-op term at The Working Group was the benefits of test driven development. Untested code is broken code, and manual testing is tedious and annoying. As your code gets more and more complicated, there will be more and more edge cases to deal with. These cannot be dealt with a single test case. Normally, you would be forced to write multiple input files and pass them each individually to the executable to see if everything worked. I don’t like having to do this. So instead, I defined my own ruleset:

ASSIGNMENT = a2p7

test: linked.mips Empty.test All.test

%.test: $(ASSIGNMENT).%.in
    @echo ----------- $* ------------------
    java mips.array linked.mips < $^

%.mips: %.asm
    java cs241.binasm < $*.asm > $*.mips

linked.asm: $(ASSIGNMENT).asm print.asm
    cat $^ > $@

This one merits a bit of explanation. The default task here is test, and it has no recipe associated with it. Instead, it just makes sure all of its dependencies are up to date. Empty.test and All.test here aren’t actual files. They’re simple methods of referring to another rule, in this case the %.test implicit rule.

The %.test implicit rule depends on a corresponding input file. For example, Empty.test will rely on a2p7.Empty.in, which will then be passed in as input to the executable. The result from running looks like this:

$  make
----------- Empty ------------------
java mips.array linked.mips < a2p7.Empty.in
Enter length of array: MIPS program completed normally.
$01 = 0x000001c4   $02 = 0x00000000   $03 = 0x00000000   $04 = 0x00000000
$05 = 0x00000000   $06 = 0x00000000   $07 = 0x00000000   $08 = 0x00000000
$09 = 0x00000000   $10 = 0x00000000   $11 = 0x000001c4   $12 = 0x00000000
$13 = 0x00000000   $14 = 0x00000000   $15 = 0x00000000   $16 = 0x00000000
$17 = 0x00000000   $18 = 0x00000000   $19 = 0x00000000   $20 = 0x00000000
$21 = 0x00000000   $22 = 0x00000000   $23 = 0x00000000   $24 = 0x00000000
$25 = 0x00000000   $26 = 0x00000000   $27 = 0x00000000   $28 = 0x00000000
$29 = 0x00000000   $30 = 0x01000000   $31 = 0x8123456c
----------- All ------------------
java mips.array linked.mips < a2p7.All.in
Enter length of array: Enter array element 0: Enter array element 1: Enter array element 2: 123
0
-456
MIPS program completed normally.
$01 = 0xfffffe38   $02 = 0x00000003   $03 = 0x00000001   $04 = 0x00000000
$05 = 0x00000000   $06 = 0x00000000   $07 = 0x00000000   $08 = 0x00000000
$09 = 0x00000000   $10 = 0x00000000   $11 = 0x000001d0   $12 = 0x00000000
$13 = 0x00000000   $14 = 0x00000000   $15 = 0x00000000   $16 = 0x00000000
$17 = 0x00000000   $18 = 0x00000000   $19 = 0x00000000   $20 = 0x00000000
$21 = 0x00000000   $22 = 0x00000000   $23 = 0x00000000   $24 = 0x00000000
$25 = 0x00000000   $26 = 0x00000000   $27 = 0x00000000   $28 = 0x00000000
$29 = 0x00000000   $30 = 0x01000000   $31 = 0x8123456c

The echo line above that simple outputs the name of the test before running it so it looks nicer on the console. The @ before the echo prevents make from displaying the command, otherwise it would look like this:

$  make
cat a2p7.asm print.asm > linked.asm
java cs241.binasm < linked.asm > linked.mips
echo ----------- Empty ------------------
----------- Empty ------------------

Submitting to Marmoset from the Commandline

Just for kicks and because I wanted to mess around with Mechanize, I built a ruby script which will submit to Marmoset for me. You can try it out yourself if you have Ruby and Rubygems by running

sudo gem install marmoset

Or you can browse the code yourself here: http://github.com/jlfwong/MarmosetSubmit

Here’s how you can integrate the submission process into your Makefile:

submit: $(ASSIGNMENT).asm
    marmoset -u jlfwong -c cs241 -a $(ASSIGNMENT) -f $(ASSIGNMENT).asm

For now, it only submits - there’s no way of checking to see if it was successful or running the release tests. I can build that if people are really interested in that, or you can fork the github repo and do it yourself.

Well, that’s all for now. Time to study up for Electrodeath.

If you liked reading this, you should subscribe by email, follow me on Twitter, take a look at other blog posts by me, or if you'd like to chat in a non-recruiting capacity, DM me on Twitter.


Zerø Wind Jamie Wong
Up next DFAs and Graphviz DOT October 16, 2010