Xdb<T>
✖ mymake
This assignment is worth 250 points total.
Please read this assignment carefully and follow the instructions EXACTLY.
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.
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
.
Must be ported from Lab 3 Part 3, not Part 2; it is important that your
implementation uses the
std::ofstream& operator<<
/std::ifstream& operator>>
overloads for
serializing the generic class.
The XdbReader<T>
, XdbWriter<T>
, and Xdb<T>
class templates should
work to generically serialize objects of any type to a file on disk, not
just MdbRec
s.
Do not define the die
macro in xdb.h
; this should be treated as a
general-purpose C++ library, and it’s bad practice to force
namespace-less macros upon users.
Define everything in namespace xdb
(as used by the type aliases).
xdb.h
.mymake
build automation toolWork 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.
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.
Every exception class should ultimately inherit from std::exception
.
std::exception
.Most of these are caught as Fatal
or NonFatal
in mymake.cpp
; make
this work using inheritance.
TargetNothingToDo
and TargetUpToDate
should be caught as
NonFatal
.Define everything in namespace exception
, which should be inside
namespace mymake
(as used by mymake.cpp
).
It’s good practice to mark member functions of an exception noexcept
when possible, to indicate to the compiler that they themselves won’t
throw an exception—but we won’t require this.
std::string
, consider the
possibility of an std::bad_alloc
.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.
throw
an exception::ParseError
.throw
an exception::ParseError
.comm_ln
first, then assign_ln
, and finally rule_ln
.exception::ParseError
with a message of your choosing
if a line goes unmatched.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:
Rule
object (rather than replace them).There are some addtional things you need to do for the special target .PHONY
:
struct Rule
has a boolean flag phony
, which should be set if the rule is for a phony target..PHONY
, as well as .PHONY
itself, is a phony target.
.PHONY
, clean
, and all
are phony targets..PHONY
should never be set as default_rule
. If .PHONY
is the only
target encountered, exception::NoTargets
should be thrown.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.
Parse the MyMakefile such that ./mymake -p
prints as expected.
Throw the pertinent exceptions when necessary.
This includes exception::ParseError
as specified above, as well as
exception::NoMyMakefile
if it is detected that the MyMakefile doesn’t
exist, or exception::NoTargets
if no target is parsed from the
MyMakefile.
exception::ParseError
’s reason phrase only needs to be specific enough
to distinguish from other places where exception::ParseError
is thrown.
std::filesystem::exists
is one way to check if a file exists.
Though no specific implementation is required, we recommend keeping track
of variable values with the vars
map declared in the skeleton code. Note
that the initializer sets some default values which will be relevant in
Part 2c.
Note that the given struct Rule
is designed to guarantee an ordered list
of unique dependencies, so there should be no special handling on your part
for repeated dependencies; just use the provided push_dep()
member.
$ ./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
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.
./mymake -p
now prints with any necessary
commands and dependencies added.We suggest implenting this logic separately from the parsing you did in Part 2b.
That is, after you are done with parsing, iterate over the rules
map
to add the implicit commands/dependencies as needed.
You may find std::filesystem::path
’s stem()
and extension()
members
helpful.
(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
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).
If there is no rule for the target being built, check if it already exists
as a file; if it doesn’t, you should throw
exception::TargetNoRule
,
otherwise (if the file does exist) throw
exception::TargetNothingToDo
.
Attempt to build all target dependencies in order before building the target itself.
If a circular dependency is detected, then log the occurrence with some
message to std::cerr
and skip the dependency.
If building a dependency throws an exception::NonFatal
, then skip it
quietly.
If the target isn’t phony, the target file exists, all dependencies
exist, and all were found to be last modified earlier than the target file
(after attempting to build them), then it is up-to-date; throw
exception::TargetUpToDate
.
If the target isn’t up-to-date, there are no commands to build the target
(i.e. if the target is phony or implicit rule generation couldn’t find a
source file), and no dependencies were built for it, then throw
exception::TargetNothingToDo
.
exception::NonFatal
was thrown.If all checks pass, then execute all commands for the target in order
using /bin/sh
(the system’s default shell command interpreter).
Print the command to std::cout
before invoking the shell.
If any report a non-zero exit status, then throw
exception::TargetCommandFailed
.
std::filesystem::last_write_time
is useful for checking when a file was
last modified.
std::system
is the easiest way to execute commands using /bin/sh
.
WEXITSTATUS
macro (from sys/wait.h
) for
deciphering the exit status of the command from the return value; e.g.:int wstatus = std::system(...);
if (WEXITSTATUS(wstatus) != 0)
...
(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
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.
Successful execution of ./mymake
creates/overwrites “.mymake.cache”
if it doesn’t already exist, is out-of-date, or is empty/ill-formatted
(such that no rules could be read); otherwise the MyMakefile isn’t
opened at all. In any case, behavior should be the same as in Part 2d.
Update the Makefile CXXFLAGS
with -I ../part1
and include xdb.h
in
maker.cpp
.
xdb.h
as a dependency to the Makefile; we’re treating it
as an external library.Overload std::ofstream& operator<<
and std::ifstream& operator>>
for
serializing an std::pair<std::string, Rule>
, so that you may use
xdb::XdbReader<std::pair<std::string, Rule>>
/
xdb::XdbWriter<std::pair<std::string, Rule>>
as specified.
The serialization format is completely up to you.
As in Lab 3, don’t worry about error-checking inside these operators;
let the caller be responsible for checking the status of the stream
afterward. This includes formatting—don’t bother setting the failbit
.
Defining operator overloads for a namespaced class is easiest done
inside the same namespace; we recommend defining yours inline
in
maker.h
.
If you are handling the optional edge cases of the cache file being
out-of-date, recall that only non-phony
rules may be implicit. Among that subset, those with zero commands
did not have a suitable source file, so if it does now, the cache is
out-of-date. Conversely, those whose implicit_dep
member is set true
did have a suitable source file, so if it doesn’t now,
the cache is out-of-date.
Recall the std::filesystem::*
library functions you’ve used up to
this point; they’re all pretty helpful for this part.
(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