Project 3: Tiny Make

Project 3: Tiny Make

In this project you will implement a simplified version of make called tinyMake, which has enough functionality to process simple Makefiles containing rules and variables. This writeup provides a short description of the features you must implement. The features of tinyMake are intended to mimic the corresponding features of GNU make (but many GNU make features are not included in this project). If you are unsure how a particular feature should work, you can run make under Linux or consult the manual for GNU make. However, you don't need to match every tiny feature of GNU make, such as the exact text and ordering of error messages; any behavior that satisfies the general description below is OK.

Rules

A Makefile for tinyMake contains rules and variable definitions. A rule has the following syntax:

target target ... : prereq prereq ...
        recipe
        recipe
        ...

The first line of the rule names one or more targets and one or more prerequisites, with a colon between the last target and the first prerequisites. This line indicates that each of the targets depends on each of the prerequisites. Targets and prerequisites are separated by white space; they may not contain space characters internally. Each line after the first one consists of a tab character followed by a recipe. A recipe is a shell command to execute to rebuild a target.

A rule need not contain any recipes. A target name may appear in multiple rules, but if so, only one of those rules may contain recipes.

Variables

TinyMake must support simple variables. A variable is defined with a line in the Makefile having the following syntax:

name = value

Everything to the left of the equals sign (except for spaces) is treated as the variable's name, and everything to the right is treated as the variable's value. There must not be any spaces within name.

A variable value can be substituted anywhere in a Makefile, using the syntax

$(name)

In particular:

  • The name and/or value for a variable definition may include the values of other variables (but there must not be loops where the name or value of a variable depends on itself). This feature may be used to define variables with names that include spaces.

  • Variables may be used in any part of a rule, including targets, prerequisites, and recipes. Variable values are substituted before parsing the names of targets and prerequisites. For example,

    TARGETS = a b
    PREREQS = d e
    $(TARGETS) c: $(PREREQS) f

    is equivalent to

    a b c : d e f
  • Variables in recipes are not expanded until just before the recipe is executed (the variable's value may be defined after the recipe is parsed).

  • A non-existent variable expands to an empty string.

  • The sequence $$ is replaced by a single $

Building a Target

To rebuild a target, tinyMake should performs the following steps:

  • Recursively build all prerequisites for the target.

  • Once all prerequisites have been built, determine whether the target is out of date. The target is out of date if one of the following conditions is true:

    • There exists no file with the target's name.

    • The target has at least one prerequisite for which there exists no corresponding file.

    • At least one of the prerequisite files has a last-modified time (the st_mtim value returned by the stat system call) later than the last-modified time of the target.

  • If the target is out of date, then execute the recipes for the target. Do this by invoking bash in a subprocess with the -c option and the recipe. You should use fork and execvp system calls directly (e.g., don't use the system library function because that could create quoting problems with recipes that use special characters). Each recipe executes in a separate shell invocation.

Command-Line Options

TinyMake should support the following command-line syntax:

tinyMake -f makefile -j concurrency target target ...

The -f option specifies the Makefile to use for the build; use Makefile if no -f option is specified. The -j option specifies how many recipes can run in parallel during the build (see below); if not specified, it defaults to 1. The target options, if present, specify one or more targets to build; if no target is specified on the command line, then the default target is the first one encountered in the Makefile.

Concurrency

If tinyMake is invoked without the -j option, then only one recipe executes at a time. If -j is specified, then tinyMake will execute recipes for different targets concurrently, up to the limit specified by -j. Each recipe executes in a different shell subprocess. Parallelism is limited by the following constraints:

  • Recipes for the the same target must execute sequentially.

  • The recipes for a target cannot execute until all recipes for its prerequisites have executed (more precisely, the up-to-date check for a target cannot be performed until all prerequisites have been built).

Error Handling

There are numerous errors that can occur while parsing the Makefile or building targets. In most cases, you can just print an error message and exit the build. For example, if a recipe fails (its subprocess exits with a status other than 0), print a message on standard error and exit.

TinyMake should follow the behavior of make and include file names and line numbers in error messages to help debugging. For example, if a Makefile line doesn't contain a ":" or "=" and doesn't start with a tab, you should print a message like

Makefile:23: *** missing separator.  Stop.

In this example, Makefile is the name of the Makefile containing the erroneous line, and 23 is the number of the offending line in the file (the first line is 1). Similarly, if a recipe fails, you should print a message like

tinyMake: *** [test/test.mk:69: fail] Error 22

In this case, test/test.mk is the name of the Makefile, the recipe that failed was on line 69, fail is the name of the target for which the recipe was run, and 22 is the exit status returned by the recipe.

In general, make your error messages as close to those from make as you can.

Testing TinyMake

The starter repo for this project contains a collection of tests for tinyMake. These are intended to be helpful, by indicating what kinds of problems you need to work on. Your implementation does not need to produce exactly the same output as the tests (e.g. the contents and order of error messages). You can run the entire suite by typing the following command in the top-level tinyMake directory:

./run_tests

This script assumes that the tinyMake executable is in that directory and has the name tinyMake. The script will print error messages if tests fail; you should then be able to go back and run the failing tests manually to debug (each test runs a shell command and checks the output). The actual test cases are in the file test_cases; hopefully this file is reasonably self-explanatory.

You may also find it helpful to run builds manually using the Makefiles from the test suite. These Makefiles are all in the subdirectory test. Most of the testing targets are in the file test/test.mk. The other Makefiles, such as test/overrideErr.mk, all generate errors during parsing.

Additional Notes and Requirements

  • This project must be implemented in C++. Use the -Wall and -Werror compiler options as for Projects 1 and 2.

  • I will create a private GitHub repository for your team to use and send you information about this repository. Create a branch in this repo named project3 and use this branch for all of your work on the project.

  • You may not use a parser generator or any existing libraries to help with parsing. In general, you should be building from scratch, but you may use any of the C++ std:: classes as well as C library functions. If you have any questions about what existing packages it is OK to use, please check with me.

  • Any line in a Makefile whose first nonspace character is '#' is treated as a comment.

  • Print each recipe on standard output before executing it, similar to how make works.

  • For at least one non-trivial source file, write the top-level declarations and interface comments before you fill in any of the method bodies. Create a commit on the project3 branch whose only change is the skeletal version of this file, and tag that commit commentsBeforeCode3. Make sure that the commit message also includes the name of this file.

  • As in the past, please use 4-space indents and keep lines to no more than 80 characters in length.

  • You should enable line-buffering for both stderr and stdout (otherwise tests may fail). You can do this by adding the following two lines to your main function:

    setvbuf(stdout, NULL, _IOLBF, 0);
    setvbuf(stderr, NULL, _IOLBF, 0);

Submitting Your Project

To submit your project, push all of your changes to GitHub on the project3 branch and then create a pull request. The base for the pull request should be your master branch (which has nothing on it except your initial commit) and the comparison branch should be the head of your project3 branch. Use "Project 3" as the title for your pull request. If your project is not completely functional at the time you submit, describe what is and isn't working in the comments for the pull request.

Late Days

If you are planning to use late days for this project please send me an email before the project deadline so that I know how many late days you plan to use.