CMPSC 311, Spring 2013, Project 4

Posted Feb. 11, 2013.  Due Feb. 22, 2013, by 11:55 pm on ANGEL.  25 points.

The goal of this project is to implement part of the make utility.  Since it isn't really make, we'll call it hake, a fishy version of make.  Some later projects will add more functionality to the program. 

You should create a project4 directory along the lines of Projects 2 and 3.  Again, if you have any problems using Unix, the editors or the compilers, don't hesitate to ask for help.  Otherwise, do this project on your own.



Reading



A quick overview of Make

The GNU Make Manual describes a makefile rule as follows:

        target ... : prerequisite ...
                recipe
                ...
                ...

The target-prerequisite line describes dependencies among files.  Each of the target files depends on all of the prerequisite files.  If any target is older than any prerequisite, or if any target doesn't exist, the recipes are used to create the targets.  Files can be both targets and prerequisites, though not in the same rule, and rules are checked recursively.

The recipe lines start with a tab character.  A recipe is a command to execute; the sequence of recipes may or may not create a file with the target name.

King (Sec. 15.4) doesn't really specify what to call the parts of a rule.

A goal is one of the targets that has been selected on the make command line.  The default goal is the first target listed in the makefile.  Goals are specified on the command line as operands to make; if there are none, the default goal is used.  If multiple goals are listed on the command line, the relevant targets are checked (and possibly rebuilt) in the order given on the command line.  Thus, with a typical makefile, "make prog" will recompile as little as necessary for prog, while "make clean prog" will recompile everything needed for prog.  Of course, that assumes a rule with the target clean (see the Makefile example that follows).

Some excerpts from the GNU Make Manual:
There are some constraints when creating a makefile.  Some of these constraints can only be detected or enforced in the second phase.
For this project, you need to open a file (maybe more than one) as specified by the defaults, by the command line, or by an include directive.  You need to distinguish line types, remove comments, and act on the include directives.  An example is given below.

One absolutely essential feature is that your program should not read any file twice.  Here's the simplest example, with the filename bogus,
include bogus
Implementations that go into infinite recursion will receive a very low grade.  An example is given below.



Starter kit
The only files that are absolutely required to be turned in are your versions of  README, Makefile and hake.c.  If you decided not to modify cmpsc311.[ch] and names.[ch], or if you merged them into hake.c, then you don't need to turn them in separately.

For grading purposes, we will run the command
make hake-sun hake-gcc hake-lint
on Solaris, to verify that the program compiles correctly (produces no errors or warnings).  Tests will be run with the executable file produced by "make hake-gcc" on  Linux.



Some specifications for Hake

command format
  hake -h               print help
  hake                  default files, default goal
  hake -v               enable more printing; verbose mode
  hake -f filename      specify an input file; default is hakefile or Hakefile
  hake f1 f2            specify goals f1 and f2

input file format -- six line types
  # comment
  include filename
  macro_name = macro_body
  target : source ...
  [tab]recipe
  (empty)

macro usage, in the input files
  macro_name = macro_body
  ${macro_name} expands to macro_body
  macro expansion occurs only once per line
  macro expansion occurs before diagnosing the line as one of the six line types

The three characters # = : cannot be used in a filename or macro name.

The following are errors (with sample input lines)
  an empty filename        include
  an empty macro_name      = macro_body
  an empty target          : source

Comments extend from # to the end of the line, and can start anywhere.

Whitespace (spaces and tabs) can appear before or after = and :, and is then ignored, as long a tab is not the first character on the line.

Examples of include lines, good and bad, with explanation
  include                       error - no filename
  includefilename               error - must have a space or tab after include
  include filename              ok
  include[tab]filename          ok
  include  filename             ok - two spaces are same as one
   include filename             ok - ignore leading spaces
  [tab]include filename         this is a recipe
  include file name             error - only one filename allowed
  include 'file name'           ok, but quotes are not part of the file name
  include "file name"           ok, same

Examples of macro lines, good and bad, with explanation
  SRC = file1.c file2.c         ok, body is "file1.c file2.c"
  LIB =                         ok, body is empty string ""
  INC = $(SRC)                  this is an error - wrong syntax for ${SRC}
                                  and, even when corrected, indicates the wrong files

On any line,



Some examples
% make hake-gcc
gcc -std=c99 -D_XOPEN_SOURCE=700 -Wall -Wextra -o hake hake.c cmpsc311.c names.c


% ./hake -a
./hake: invalid option 'a'
./hake: Try './hake -h' for usage information.


% ./hake -h
usage: ./hake [-h] [-v] [-f file]
  -h           print help
  -v           verbose mode; enable extra printing; can be repeated
  -f file      input filename; default is hakefile or Hakefile


% ls -l hakefile Hakefile testfile-1 testfile-2
ls: cannot access hakefile: No such file or directory
-rw------- 1 dheller fcse 116 Feb 11 10:50 Hakefile
-rw------- 1 dheller fcse  13 Feb 11 10:50 testfile-1
-rw------- 1 dheller fcse 408 Feb 11 10:50 testfile-2


% cat testfile-1
# testfile-1


% ./hake -f testfile-1


% ./hake -v -f testfile-1
./hake: read_file(testfile-1)
list of names: filenames
  testfile-1
./hake: read_lines(testfile-1)
./hake: testfile-1: line 1: # testfile-1


% ./hake -v -v -f testfile-1
./hake: read_file(testfile-1)
list of names: filenames
  <empty>
list of names: filenames
  testfile-1
./hake: read_lines(testfile-1)
./hake: testfile-1: line 1: # testfile-1


% ./hake -v -v -v -f testfile-1
./hake: read_file(testfile-1)
./hake: strdup(10) at 0xcc0010 from list_names_init line 34
list of names: filenames
  <empty>
./hake: malloc(16) at 0xcc0030 from list_names_append line 111
./hake: strdup(11) at 0xcc0050 from list_names_append line 114
list of names: filenames
  testfile-1
./hake: read_lines(testfile-1)
./hake: testfile-1: line 1: # testfile-1


% cat testfile-2
# testfile-2
include              # no file, but don't complain
include foo          # proper
include testfile-1   # proper
include "foo bar"    # proper
include 'foo bar'    # proper
include "foo bar'    # improper
include 'foo bar"    # improper
include "foo bar     # improper
include 'foo bar     # improper
include  foo bar"    # improper, but not caught
include  foo bar'    # improper, but not caught


% ./hake -f testfile-2
./hake: could not open input file foo: No such file or directory
./hake: could not open input file foo bar: No such file or directory
./hake: testfile-2: line 7: file name error ["foo bar']
./hake: testfile-2: line 8: file name error ['foo bar"]
./hake: testfile-2: line 9: file name error ["foo bar]
./hake: testfile-2: line 10: file name error ['foo bar]
./hake: could not open input file foo bar": No such file or directory
./hake: could not open input file foo bar': No such file or directory


% ./hake -v -f testfile-2
./hake: read_file(testfile-2)
list of names: filenames
  testfile-2
./hake: read_lines(testfile-2)
./hake: testfile-2: line 1: # testfile-2
./hake: testfile-2: line 2: include              # no file, but don't complain
  diagnosis: include
./hake: testfile-2: line 2: include but no filename
./hake: testfile-2: line 3: include foo          # proper
  diagnosis: include
./hake: read_file(foo)
list of names: filenames
  testfile-2
  foo
./hake: could not open input file foo: No such file or directory
./hake: testfile-2: line 4: include testfile-1   # proper
  diagnosis: include
./hake: read_file(testfile-1)
list of names: filenames
  testfile-2
  foo
  testfile-1
./hake: read_lines(testfile-1)
./hake: testfile-1: line 1: # testfile-1
./hake: testfile-2: line 5: include "foo bar"    # proper
  diagnosis: include
./hake: read_file(foo bar)
list of names: filenames
  testfile-2
  foo
  testfile-1
  foo bar
./hake: could not open input file foo bar: No such file or directory
./hake: testfile-2: line 6: include 'foo bar'    # proper
  diagnosis: include
./hake: read_file(foo bar)
./hake: testfile-2: line 7: include "foo bar'    # improper
  diagnosis: include
./hake: testfile-2: line 7: file name error ["foo bar']
./hake: testfile-2: line 8: include 'foo bar"    # improper
  diagnosis: include
./hake: testfile-2: line 8: file name error ['foo bar"]
./hake: testfile-2: line 9: include "foo bar     # improper
  diagnosis: include
./hake: testfile-2: line 9: file name error ["foo bar]
./hake: testfile-2: line 10: include 'foo bar     # improper
  diagnosis: include
./hake: testfile-2: line 10: file name error ['foo bar]
./hake: testfile-2: line 11: include  foo bar"    # improper, but not caught
  diagnosis: include
./hake: read_file(foo bar")
list of names: filenames
  testfile-2
  foo
  testfile-1
  foo bar
  foo bar"
./hake: could not open input file foo bar": No such file or directory
./hake: testfile-2: line 12: include  foo bar'    # improper, but not caught
  diagnosis: include
./hake: read_file(foo bar')
list of names: filenames
  testfile-2
  foo
  testfile-1
  foo bar
  foo bar"
  foo bar'
./hake: could not open input file foo bar': No such file or directory


% cat Hakefile
# comment 1

macro = body

target1 : source1 source2
    recipe 1
    recipe 2
    recipe 3

include testfile-1

# comment 2


% ./hake


% ./hake -v
./hake: read_file(hakefile)
list of names: filenames
  hakefile
./hake: read_file(Hakefile)
list of names: filenames
  hakefile
  Hakefile
./hake: read_lines(Hakefile)
./hake: Hakefile: line 1: # comment 1
./hake: Hakefile: line 2:
./hake: Hakefile: line 3: macro = body
  diagnosis: macro definition
./hake: Hakefile: line 4:
./hake: Hakefile: line 5: target1 : source1 source2
  diagnosis: target-prerequisite
./hake: Hakefile: line 6:     recipe 1
  diagnosis: recipe line 1
./hake: Hakefile: line 7:     recipe 2
  diagnosis: recipe line 2
./hake: Hakefile: line 8:     recipe 3
  diagnosis: recipe line 3
./hake: Hakefile: line 9:
./hake: Hakefile: line 10: include testfile-1
  diagnosis: include
./hake: read_file(testfile-1)
list of names: filenames
  hakefile
  Hakefile
  testfile-1
./hake: read_lines(testfile-1)
./hake: testfile-1: line 1: # testfile-1
./hake: Hakefile: line 11:
./hake: Hakefile: line 12: # comment 2


% cat bogus
include bogus


% ./hake -v -f bogus
./hake: read_file(bogus)
list of names: filenames
  bogus
./hake: read_lines(bogus)
./hake: bogus: line 1: include bogus
  diagnosis: include
./hake: read_file(bogus)




Some design choices for Hake, vs. Make

If you don't specify an input file with the -f option, GNU Make uses the default makefile names GNUmakefile, makefile and Makefile, in that order.  Hake uses hakefile and Hakefile, in that order.  GNUmakefile is an extension to the traditional Unix Make program.

Here is an excerpt from make's man page on Mac OS X, reformatted:
 
make executes commands in the makefile to update one or more target names, where name is typically a program.  If no -f option is present, make will look for the makefiles GNUmakefile, makefile, and Makefile, in that order.
 
Normally you should call your makefile either makefile or Makefile.  (We recommend Makefile because it appears prominently near the beginning of a directory listing, right near other important files such as README.)  The first name checked, GNUmakefile, is not recommended for most makefiles.  You should use this name if you have a makefile that is specific to GNU make, and will not be understood by other versions of make.  If makefile is "-", the standard input is read.

Hake should behave in the same way, with regard to files named hakefile or Hakefile, and with regard to the command-line option "-f -".

You can specify multiple files to read, by repeating the -f option.

Failure to open a file should not necessarily terminate the program.  There is also a choice about issuing a warning message.  If no -f option was given, then try to open hakefile.  If hakefile doesn't exist, then try to open Hakefile, and don't complain about hakefile.  If Hakefile also doesn't exist, then warn about no input.  If a -f option was given, naming a file that doesn't exist, that's an error.  Here, "doesn't exist" really means "can't be opened for reading".
 
Here are some additional design considerations.  Some of these refer to features that are not part of Project 4, but might be part of a later project.

GNU Make does not handle include'ed filenames with embedded spaces - it thinks there are multiple file names, even when you quote the name.  Macro expansion (not part of this project) precedes parsing the input line to decide if it starts with include, and the ability to name more than one file might be useful.  Hake allows you to quote the filename, but you can give only one filename with an include directive.

When processing an include directive, GNU Make will expand shell file name patterns and match against the current directory contents, search other directories to locate a file, and attempt to make an include'ed file that can't be found.  There is also a -include form.  GNU Make will also expand wildcard characters in filenames on target-prerequisite lines.  Hake doesn't do any of this.

GNU Make has variable definitions; Hake has macro definitions.  GNU Make uses $() for variable substitution; Hake uses ${} for macro expansion.  This is just a vocabulary difference; the concepts are the same.  GNU Make has automatic variables, such as $@; Hake doesn't.

GNU Make can deduce which recipe should be used, if you don't specify one; there is a set of implicit rules, and a matching algorithm.  Hake doesn't do that; it has only explicit rules.

Hake doesn't actually execute the recipes; it acts like Make with the -n option.

GNU Make has conditional directives, double-colon rules, and functions; Hake doesn't.

GNU Make allows phony targets, which are sometimes useful.  Since phony targets are only distinguished by their presence as a prerequisite in a rule with the target .PHONY, no extra action is necessary in this project.

Both GNU Make and Hake have comments, starting with # and going to the end of the line.  GNU Make allows line continuation as in C; Hake doesn't, or at least doesn't require it.  GNU Make leaves comments in recipes that are passed to the shell; Hake removes them.  GNU Make leaves comments in variable definitions; Hake removes them.

GNU Make allows long-form command line options, such as --file=filename in place of -f filename; Hake doesn't allow this. 

If no makefiles are found, GNU Make relies on its implicit rules; Hake just reports "no input".



When your code is complete and you are satisfied it is right, here is how to turn it in for a grade.

Login to one of the CSE Linux or Solaris systems, cd to your cmpsc311/project4 directory, maybe transfer files from your home system, and run some final tests just to make sure.

Your code should be in one or more files named hake.c, cmpsc311.c, cmpsc311.h, names.c and names.h which each start like
/* CMPSC 311, Spring 2013, Project 4
*
* Author: <your name>
* Email: <your preferred email address>
*
... <additional comment text>
*/
except, of course, your own name and address should be there.

You should have a file named Makefile, which could be the same as the one linked above.  Modify Makefile, to enter your own name and address.  Otherwise, just add to it, and don't remove any of the targets that are already there.

Pick up (i.e., download or copy) the file README, and modify it accordingly.  Any messages or comments about the project should go here.

Pick up the file wrap, and don't change it.  This contains the shell script that will bundle your project files.

Execute the command
sh wrap
which (if you have done everything right) should produce output like this.  If you made a mistake, you would get output like this instead.  You can expect some variation in the output because of different user names, file modification times, file sizes, Linux vs. Solaris, etc.

The wrap script will create a "gzipped tar file" containing the files README, Makefile, hake.c, etc., and some additional data.  The actual name of the file depends on your username.  Tar stands for "tape archive"; it accumulates a collection of files into one file, preserving ownership and date information.  gzip does file compression, and the original tar file will be recovered by using gunzip.

Login to ANGEL and put the file project-4-username.tar.gz in the ANGEL Dropbox for Project 4 (with your username substituted, of course).
The ANGEL Dropbox will be open for two days after the due date, but you really should get this in on time.  Nothing will be accepted more than two days late.

Only the most recent version in the Dropbox will be graded.



Additional Notes.
If you have questions about the assignment description or expectations, or need more examples, send mail to dheller at cse.psu.edu, with the subject line "CMPSC 311 Project 4" (this will help keep things organized).



Last revised, 11 Feb. 2013