I’m on the course staff for the systems programming course here at the University of Illinois, leading the team responsible for the our large (often 2 week) assignments. When we prepare assignments we usually write up our own solution to the assignment, then trim the solution down and provide the students with some skeleton code. In the past, this trimming down has been done by hand. Doing it by hand really sucks. It’s very easy to miss something important, and it becomes much more painful to make any changes to the assignment (the changes must be made in the solution directory and the student code directory). I wanted to alleviate this pain a bit.
What should be deployed?
We obviously want to deploy code, but it would be nice to do a few things to the code first. We probably want to stick some sort of header on every file, like
This is a pretty nice example of something that we don’t want to change on every single file every semester. It would probably also be nice to ensure that all line endings are unix line endings (maybe no one other than me actually cares about this), and to run the code through some sort of formatter (I prefer clang-format with llvm style).
For the actual code, it might be nice if we could use c
ifdefs to control what
is considered deploy code and what is considered solution code. In one of our
current assignments we push out intentionally broken code, so we can’t just mark
code as “solution code” and remove it.
Maybe something like this would be nice:
This turns out to be really easy to do. There’s a simple utility called
unifdef that just works. Run
unifdef -DDEPLOY on the solution file with
ifdef guards, and you get exactly what you wanted out:
We also probably want to copy over the Makefile for the assignment, but this doesn’t need any modification. A few of our assignments need data files or example input files and things like that. These also don’t need any modification.
Finally, it might be nice to be able to generate a reference implementation from our working solution code. This would need to be generated from an optimized build of the solution code, without debug symbols (let’s run it through strip just to be safe).
We need to pick some sort of tool that is really good at running command line tools (clang-format, dos2unix, unifdef) and really good at matching patterns in filenames. I know of at least one tool that is pretty good at both of these things. I decided to use GNU make to define these scripts.
First, lets consider what happens to any c source file.
%.c target in the deploy directory, first, depends on the existence of the
deploy directory. Then, we have to make sure the file’s directory actually
exists (we might have code in
libs/weirdthing/cool_file.c). Then, the code
unifdef and a formatter, we stick the header on it, and pipe it
dos2unix. That was easy! Well, not quite. If you aren’t familiar with
GNU make, this bit might throw you off a bit:
This is a patterned replacement (Text
in make terminology). It basically says, take the string
$@ (the target of the
rule), and replace the text
$(SOURCE_DIR). In other
words, to generate a file
deploy/test.c we would start with the file
We do the same thing for
Generating a reference implementation is also pretty straightforward:
We run make in the solution directory, then copy the executable with name given in the target into the deploy directory, then run strip.
For anything else:
Just copy the file. This rule comes last, so that make will fall through to this rule. Again, you see a bunch of text replacement going on.
You may have noticed that each of my rules is for something in the deploy directory, then I do some text replacement to get the intended original source. This is intentional. There are a few cases where I do text replacement to generate the output file name, then do text replacement again to get the input file name back, but I’m not writing the Makefile to process input files, I’m writing it so that it knows how to generate the appropriate output files (since that’s the Makefile model).
Here’s what I mean:
Using the Makefile
Our git repo looks like this:
Where each of the mp folders holds a single assignment (we call our assignments “Machine Problems”). I stuck the Makefile described above in the root of the repo, then define an assignment specific Makefile in the assignment directory.
A project specific Makefile
mp1/Makefile defines the variables the full
Makefile needs to function, and includes the full Makefile. For example:
Finally, stick a file in
code_header that defines the header to
stick on each code file.
make in the
/mp1 directory creates the folder
containing the deployable code, and a reference implementation for the
Now, my life is a lot easier.
Full Deploy.mk file
not using a gist cause I tend to accidentally delete those: