The object of this assignment is to familiarize yourself with recursion, executing UNIX commands from another program, system calls, using the file manipulation facilites of C, Makefiles, HashTables, dynamic memory allocation, more modules, makefiles and valgrind. (Return of the son of "There are still no donuts to be counted for this assignment") The assignment is due April 18th at 11:59 pm (before midnight). The assignment will be turned in electronically via the turnin command. The name for the assignment is 352assign5, so when you are ready to submit your solutions, you'll need to do: turnin 352assign5 Makefile file1 file2 ... filen You are allowed to turn in whatever files you want (assuming you wrote them all: you aren't allowed to grab anyone elses files!), but it's important that you turn in a Makefile that responds to two commands: "make memake" and "make clean". If your program can't even build, you won't get many (if any) points. When you type "make memake", it should build an executable called "memake". Your executable, memake is what we will be testing. Testcases will be avilable next week in the standard place: ~cs352/spring08/assign5/testcases Scenario: For this assignment, you will be writing your own "make" facility, called "memake" ("imake" does something else, and "myselfmake" is too long to type). It will respect a simplified format of current makefiles. You don't have to worry about handling default rules or macros within your memake. Starting memake: Upon startup, memake will look for either a file called "makefile" or "Makefile" (in that order) in the current directory. If there is no such file, then memake should exit with an error and issue the following message to the standard error. % ./memake memake: *** No targets specified and no makefile found. Stop. % echo $status 2 You are expecting the format of the makefile (or Makefile) that memake operates on to be a simple subset of the typical make formats. There will be a list of of targets and dependencies with their rules on the right hand side, followed by a bunch of UNIX commands to execute. A simple example starts on the next page: main : cstr.o hash.o main.o echo "link only" gcc -ansi -Wall -DMACHINE64 cstr.o hash.o main.o -o main cstr.o : cstr.c gcc -ansi -Wall -DMACHINE64 -c cstr.c hash.o : hash.c gcc -ansi -Wall -DMACHINE64 -c hash.c main.o : main.c gcc -ansi -Wall -DMACHINE64 -c main.c clean : /bin/rm -rf *.o main Each make rule has three main parts: the target, the list of dependencies (or just dependencies), and the list of the UNIX commands to run to build the target from the dependencies (or just build commands). In the example above: hash.o is a rule with one dependency (hash.c) and has one build command (gcc -ansi -Wall -DMACHINE64 -c hash.c). A rule has the basic format below: target : dep1 dep2 .. depn build-command1 build-command2 ... build-commandn The makefile may contain any number of rules (in the above format). IT IS IMPORTANT TO REALIZE THERE IS ALWAYS A TAB CHARACTER '\t' IN FRONT OF ALL 'build-commands". When there is no longer a '\t' character at the front of a line, we are starting a new rule. For example, consider the following simple Makefile: main : cstr.c hash.c main.c echo "compile and link in one line" gcc -ansi -Wall -DMACHINE64 cstr.c hash.c main.c -o main clean : /bin/rm -rf *.o main In the above example, the "main" rule ends when the "clean" rule starts, because the clean line starts WITHOUT A TAB character. Of course, an empty line also ends a rule (and does NOT become part of the build commands). The number of dependencies in the target can be zero. In that case, the build-commands ALWAYS execute. (I.e., "make clean" always executes the command "/bin/rm -rf *.o main"). Although some makefiles allows multiple targets on one line, we will only allow one target. Of course, there can be multiple dependencies. Consider our more complex example again: main : cstr.o hash.o main.o echo "link only" gcc -ansi -Wall -DMACHINE64 cstr.o hash.o main.o -o main cstr.o : cstr.c gcc -ansi -Wall -DMACHINE64 -c cstr.c hash.o : hash.c gcc -ansi -Wall -DMACHINE64 -c hash.c main.o : main.c gcc -ansi -Wall -DMACHINE64 -c main.c clean : /bin/rm -rf *.o main If you type "memake main", the following occurs: (1) First, memake looks for makefile then Makefile in the current directory. If it can't find it, it exits with an error. (2) Once memake finds the makefile, it goes through and parses the complete file. In other words, it sequentially scans the files, building a "rules database", where it can lookup a rule by string name. (3) Once the rules database is built, "memake" looks for a rule named "main". If there isn't one, "memake" immediately exits. (4) If there is a rule named "main", then "memake" gets the list of dependencies and list of build commands. (5) Recursively, "memake" goes through each dependency and tries to "build" each one: In other words, each single dependency becomes a target and the memake tries to build each one. (6) Once all the dependencies have been built, then memake executes the list of build commands in order. (7) Assuming nothing fails, memake exits. If there was an error at any point, memake should have exited and aborted the process. Because we are using a standard makefile format, you should be able to test you Makefiles with the real "make" to see what they do. When you have questions about what the behavior of memake or the behavior, use make as a model. DO NOT COPY ANY OF MAKE FROM OPENSOURCES: IF YOU DO, YOU WILL GET A ZERO ON THE ASSIGNMENT. THE WHOLE PURPOSE OF THIS ASSIGNMENT IS TO WRITE MAKE FOR YOURSELF SO YOU UNDERSTAND IT. One thing that is important is that rules respect the timestamp on files. For example, target "main.o" depends on "main.c". This means two things: (1) When memake tries to "build" main.o, it first sees if the dependency file "main.c" exists. If the file does NOT exist, memake will scan its rules database to see if there is a rule to build "main.c". If there is such a rule, memake recursively builds "main.c". If there is no rule, then memake will exit with an error. (2) If the file "main.c" already exists, then memake looks at the timestamp on the file. If "main.o" is older than "main.c", then main.c has probably been edited and we need to regenerate (rebuild) main.o, so we run all the build commands to build "main.o". Thoughts: The rules database should probably be implemented as a Hashtable where the key is the name of the rule, and the (dependencies, build-commands) is the value associated with the key. When you aren't sure of a behavior, model make. Use the fact that there is a make already to help you test your code. You'll probably want to use recursion to recursively build rules. You don't have to, but the alternative is to make a Deque of targets to build and everytime you have to build a new target, you have to put all your new targets on the deque. You should make sure your memake is memory clean (i.e., run valgrind on it). For file manipulations, use fstat (man -k fstat) Memake takes either 0 or 1 command-line arguments: % memake # find first rule in makefile and tries to make that % memake sometarget # find rule sometarget in makefile and make that % memake 1 2 # ERROR! output to stderr and exit with error val usage: memake [target]