labs

c2cpp lab4: Xdb<T>mymake

This assignment is worth 250 points total.

Please read this assignment carefully and follow the instructions EXACTLY.

Submission

Do NOT create any additional subdirectories. Work inside the existing part*/ subdirectories.

Ensure the Makefile for each part build all executables when you run make.

Please refer to the lab submission instruction for other requirements.

Please check your code with valgrind. You will be penalized if you have any memory leaks or errors.

Part 1: Templatizing the Mdb classes

Work from the part1/ directory for this part.

The skeleton code contains the same mdb-add.cpp and mdb-cat.cpp programs from Lab 3. It also contains pared down mdb.h and mdb.cpp files, neither of which should be modified for this lab. You might notice that mdb.h includes a header xdb.h, and contains the following type aliases:

using MdbReader = xdb::XdbReader<MdbRec>;
using MdbWriter = xdb::XdbWriter<MdbRec>;
using Mdb = xdb::Xdb<MdbRec>;

Your job is to create xdb.h so that mdb-add.cpp and mdb-cat.cpp work exactly as they had in Lab 3. You should port implementations from the Lab 3 Part 3 solution (either ours or yours), accepting a generic type in place of MdbRec.

Requirements

Hints

Part 2: Implementing the mymake build automation tool

Work from the part2/ directory for this part.

You’re going to develop mymake, a utility which performs a subset of GNU make’s functionality—though instead of a Makefile, it uses a MyMakefile. By the end of this lab, your mymake should be able to build itself, as well as all your previous labs.

You may NOT modify the provided mymake.cpp. Additionally provided is skeleton code in maker.h and maker.cpp which defines class Maker and struct Rule. You’ll be modifying maker.cpp. The provided maker.h is complete. You do not have to modify it, but you can if it suits your implementation. However, you may not modify the existing data members.

Our solution executable is available at /home/jae/cs4995-pub/bin/mymake-ref; you’re free to play with it and see the expected behavior.

As you’re going, do not fixate on every possible edge case, or email the listserv about every such case. There are many. All that’s required is that your implementation meets the criteria specified; you will not be tested on functionality outside of what is detailed explicitly by the prompt. If you really are unsure of the expected/required behavior, please test with the solution executable before reaching out to the listserv.

Part 2a: Defining custom exceptions

First, define the following exceptions to be caught in mymake.cpp:

ParseError(int lineno, const std::string& reason)
NoMyMakefile()
NoTargets()
TargetNoRule(const std::string& target)
TargetCommandFailed(const std::string& target, int exit_status)
TargetNothingToDo(const std::string& target)
TargetUpToDate(const std::string& target)

These should be defined entirely inline in exception.h, which is already included by mymake.cpp. Each should have a distinct what() message which details the parameters passed to its constructor (e.g. ParseError’s what() should include a line number and reason phrase); the exact formatting of your messages is up to you, so long as the situation is clear when an exception is thrown.

Requirements

Hints

Part 2b: Parsing with std::regex

To begin, let’s parse the MyMakefile.

This should be done entirely in Maker’s constructor. Provided is a starting point in maker.cpp, where we’ve provided std::regex definitions for matching the necessary expressions:

static const std::regex comm_ln("^\\t(.*)$");
static const std::regex assign_ln("^(.*?)=(.*)$");
static const std::regex rule_ln("^(.*?):(.*)$");
static const std::regex var("\\$\\((.*?)\\)");

comm_ln matches any line that begins with a tab character as a command line, and captures everything after the tab. assign_ln matches any line with an ‘=’ as variable assignment, and captures everything before and after it (separately). rule_ln matches any line with a ‘:’ as a rule definition, and captures everything before and after it (the same as variable assignment). Finally, var does not match an entire line, but rather matches any instance of variable usage (the same syntax as in a normal Makefile), and captures the variable name. This syntax is of the form “$(some_var)”.

You’ll need to read line-by-line through the MyMakefile and parse the relevant information into the Maker’s rules map, as well as set the default_rule pointer to point to the element of the map which was first encountered.

Any leading or trailing whitespace of any target, dependency, or variable name should be ignored. You shouldn’t need to worry about commands though, as the shell program we’ll pass them to should ignore whitespace anyway. To this end, we’ve defined some helper functions in helper.h for your use. helper::trim trims all leading and trailing whitespace from a passed string and returns the new string. helper::split takes a string and returns a vector of substrings that were separated by whitespace. You’re free to ignore or modify this header as you like if it doesn’t suit your implementation; it’s provided purely for convenience.

Some notes about parsing rules:

There are some addtional things you need to do for the special target .PHONY:

Note that unlike a normal Makefile, our MyMakefile will require that a variable be assigned before it is used, so you only need one pass through the file. If there are multiple assignments of a variable, it is unspecified which value will be used on an occurrence, so there is flexibility in how you implement this. If an unassigned variable is referenced, treat it like the value is an empty string.

For testing, mymake accepts a -p flag which will direct it to dump the parsed contents of Maker rather than attempt to build anything.

Requirements

Hints

Example usage

$ ./mymake
mymake: *** No mymakefile found.  Stop.
$ > MyMakefile
$ ./mymake
mymake: *** No targets.  Stop.
$ echo gobbledygook > MyMakefile
$ ./mymake
MyMakefile:1: *** missing separator.  Stop.
$ echo '   = novar' > MyMakefile
$ ./mymake
MyMakefile:1: *** empty variable name.  Stop.
$ echo -e '\techo norule' > MyMakefile
$ ./mymake
MyMakefile:1: *** commands commence before first target.  Stop.
$ cp Makefile MyMakefile
$ ./mymake -p
mymake: maker.o

(phony)
all: clean mymake

mymake.o: maker.h exception.h

(phony)
.PHONY: clean all

maker.o: maker.h exception.h helper.h

(phony)
clean:
	rm -f *.o *~ a.out core mymake

Part 2c: Generating implicit rules

In addition to what was parsed, we’d like to generate some implicit rules that automate the build process when commands aren’t specified. These are the implicit rules which allow you to write a Makefile without writing out every single g++ command. This should be handled entirely in Maker’s constructor.

Any target which has no commands specified, and which is not phony, may have an implicit command and dependency added. In all cases, the implicitly added dependency should be positioned before existing ones (inserted at the front of the list), unless it is already a dependency (because it was specified explicitly). It’s easiest to use the provided push_impl_dep() member of Rule for implementing this—it also marks the Rule as relying on an implicit dependency by setting its implicit_dep member, which will be useful in Part 2e.

If the target is a .o object file, you should attempt to build it from source. Check for the presence of a corresponding .cpp or .c file. If both exist, default to the .cpp file. If neither is found, do not add an implicit rule.

If the target is some_target.o and a some_target.cpp C++ source file is found, you should add the following command:

$(CXX) $(CXXFLAGS) -c -o some_target.o some_target.cpp

Where $(*) are replaced with the corresponding variable values.

If a some_target.c C source file is found instead, the command should be:

$(CC) $(CFLAGS) -c -o some_target.o some_target.c

In either case, the .cpp/.c source file should be added as a dependency.

If the target has no .o suffix, we’ll treat it as an executable. If the target is some_target, you should first check to see if there exists a rule to build some_target.o. If there is one, then the command should look like:

$(CC) $(LDFLAGS) some_target.o [dep1 dep2 ...] $(LDLIBS) -o some_target

Where [dep1 dep2 ...] are any existing dependencies of some_target. You should add some_target.o as a dependency in this case.

If there is no rule for some_target.o, then instead you should attempt to build the executable directly from source files. Given the target some_target, check for the presence of some_target.cpp or some_target.c. If neither is found, skip this rule. Otherwise, add a command that looks like one of the following:

$(CC)  $(CFLAGS)   $(LDFLAGS) some_target.c   [dep1 dep2 ...] $(LDLIBS) -o some_target

or

$(CXX) $(CXXFLAGS) $(LDFLAGS) some_target.cpp [dep1 dep2 ...] $(LDLIBS) -o some_target

You should add the .cpp/.c source file as a dependency in this case.

Requirements

Hints

Example usage

(continuing from last example after re-compiling mymake)
$ ./mymake -p
mymake: mymake.o maker.o
	g++  mymake.o maker.o  -o mymake

(phony)
clean:
	rm -f *.o *~ a.out core mymake

(phony)
.PHONY: clean all

(phony)
all: clean mymake

maker.o: maker.cpp maker.h exception.h helper.h
	g++ -g -Wall -std=c++17 -c -o maker.o maker.cpp

mymake.o: mymake.cpp maker.h exception.h
	g++ -g -Wall -std=c++17 -c -o mymake.o mymake.cpp

Part 2d: Building a target

With our Maker now fully constructed, it’s time to implement the make() member to build targets. Provided in the skeleton Maker class definition is a private make() overload which is invoked by the public one. It is recommended that you implement this member function by the provided signature, though you are still allowed to modify member functions as you wish. In any case, your implementation should be in maker.cpp.

The intent behind the provided make() signature is to catch circular dependencies by tracking the targets seen recursively in a set; it returns a boolean to indicate if a circular dependency was hit. For reference, a circular dependency might look like this:

target1: target2

target2: target1

If uncaught, this would recurse infinitely as each target tries to build the other before building itself.

Note that on top of this, GNU’s make tracks which targets have already been built overall on a given run (not just recursively, as is the case for circular dependencies), so that it may quietly skip a dependency if it’s already been handled earlier at some point. For simplicity, mymake will omit this feature, so a target may be built multiple times on a single run—just not recursively. In practice, this only makes a difference for phony targets (where the target isn’t a real file).

Requirements

Hints

int wstatus = std::system(...);
if (WEXITSTATUS(wstatus) != 0)
  ...

Example usage

(continuing from last example after re-compiling mymake)
$ ./mymake all
rm -f *.o *~ a.out core mymake
g++ -g -Wall -std=c++17 -c -o mymake.o mymake.cpp
g++ -g -Wall -std=c++17 -c -o maker.o maker.cpp
g++  mymake.o maker.o  -o mymake
$ touch maker.cpp
$ ./mymake
g++ -g -Wall -std=c++17 -c -o maker.o maker.cpp
g++  mymake.o maker.o  -o mymake
$ rm mymake.o
$ ./mymake
g++ -g -Wall -std=c++17 -c -o mymake.o mymake.cpp
g++  mymake.o maker.o  -o mymake
$ touch maker.h
$ ./mymake
g++ -g -Wall -std=c++17 -c -o mymake.o mymake.cpp
g++ -g -Wall -std=c++17 -c -o maker.o maker.cpp
g++  mymake.o maker.o  -o mymake
$ mv mymake ..
$ ../mymake
g++  mymake.o maker.o  -o mymake
$ ./mymake
mymake: 'mymake' is up to date.
$ ./mymake noexist
mymake: *** No rule to make target 'noexist'.  Stop.
$ ./mymake mymake.cpp
mymake: Nothing to be done for 'mymake.cpp'.
$ echo 'target1: target2' >> MyMakefile
$ echo 'target2: target1' >> MyMakefile
$ ./mymake target1
mymake: Circular target2 <- target1 dependency dropped.
mymake: Nothing to be done for 'target1'.
$ echo fail: >> MyMakefile
$ echo -e '\texit 22' >> MyMakefile
$ ./mymake fail
exit 22
mymake: *** [fail] Error 22
$ echo $?
22

Part 2e: Caching target-rule pairs with xdb::XdbReader/Writer<T>

At this point, you should have a fully-functioning mymake implementation. Now let’s extend it with a feature that make doesn’t have.

You might recall it took significant effort to parse through the MyMakefile on Maker construction, between RegEx text processing, variable mapping, and the implicit rule logic. This wouldn’t be a proper C++ project if we didn’t over-optimize, so you’re going to try to avoid this overhead by caching the rules map to a file on disk upon successful construction.

The file should be called “.mymake.cache”. The reference executable names the cache file as “.mymake-ref.cache” so that it does not conflict with your cache file.

At the beginning of Maker’s constructor, you should check:

The cache is also out-of-date if either:

You are encouraged to handle these two cases (and we give hints on how to do it below), but it’s not required for full credit.

If the cache file is up-to-date, you should load the cache contents into rules and bypass the normal MyMakefile-processing logic. Remember to set the default_rule pointer as well. If no rules could be read from the cache file for whatever reason, proceed to MyMakefile-processing. If you ended up processing the MyMakefile, write out the contents of rules to the cache file.

The formatting of the cache is up to you entirely. All that’s specified is that you must use xdb::XdbReader<std::pair<std::string, Rule>> / xdb::XdbWriter<std::pair<std::string, Rule>> for reading/writing it (with that specific template parameter).

Behavior should be the exact same as before—you will only notice a difference by tracing the files opened by mymake, as done in the example usage using strace (a “system call” tracing utility). There is only a noticeable speed improvement for very large MyMakefiles.

The behavior is undefined if the cache file is corrupt/invalid. This does NOT excuse memory leaks, other memory errors, or crashing in such a case.

Requirements

Hints

Example usage

(continuing from last example after re-compiling mymake)
$ cp Makefile MyMakefile
$ ls .mymake.cache
ls: cannot access '.mymake.cache': No such file or directory
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, "MyMakefile", O_RDONLY) = 3
openat(AT_FDCWD, ".mymake.cache", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
mymake: 'mymake' is up to date.
$ ls .mymake.cache
.mymake.cache
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
mymake: 'mymake' is up to date.
$ touch MyMakefile
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, "MyMakefile", O_RDONLY) = 3
openat(AT_FDCWD, ".mymake.cache", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
mymake: 'mymake' is up to date.
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
mymake: 'mymake' is up to date.
$ mv mymake.cpp deleted_source
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
openat(AT_FDCWD, "MyMakefile", O_RDONLY) = 3
openat(AT_FDCWD, ".mymake.cache", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
mymake: 'mymake' is up to date.
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
mymake: 'mymake' is up to date.
$ mv deleted_source mymake.cpp
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
openat(AT_FDCWD, "MyMakefile", O_RDONLY) = 3
openat(AT_FDCWD, ".mymake.cache", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
mymake: 'mymake' is up to date.
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
mymake: 'mymake' is up to date.
$ > .mymake.cache
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
openat(AT_FDCWD, "MyMakefile", O_RDONLY) = 3
openat(AT_FDCWD, ".mymake.cache", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
mymake: 'mymake' is up to date.
$ strace '-o|grep open.*ake' ./mymake
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
mymake: 'mymake' is up to date.
$ strace '-o|grep open.*ake' ./mymake all
openat(AT_FDCWD, ".mymake.cache", O_RDONLY) = 3
rm -f *.o *~ a.out core mymake
g++ -g -Wall -std=c++17 -I ../part1 -c -o mymake.o mymake.cpp
g++ -g -Wall -std=c++17 -I ../part1 -c -o maker.o maker.cpp
g++  mymake.o maker.o  -o mymake

Good luck!


Last updated: 2023-06-07