Advanced C Programming

Spring 2024 ECE 26400 :: Purdue University

This is a PAST SEMESTER (Spring 2024).
Due 2/16

Make

Learning goals

You will learn the following concepts/skills:

  1. Build systems – Create make files for compilation and testing.

Starter code

You will create your Makefile from scratch (i.e., starting with a blank file).

About make and the Makefile

The make tool is used to build software projects and perform other operations (e.g., running tests, cleaning up intermediate files, etc.). It is widely used by real-world projects in industry and open source projects.

Instructions for how to build a project and perform other actions are given in a file called Makefile. That file contains instructions for how to build the executable, as well as rules for how to build or generate any intermediate files (e.g., object files).

Structure of a Makefile

A Makefile is a text file named “Makefile” (exactly). It may contain variables, rules, and a .PHONY target.

# VARIABLES
LHS=RHS
LHS=RHS


# RULES
target: prerequisites…
    action
    action
    

target: prerequisites…
    action
    action
    

# .PHONY TARGET
.PHONY: target target 

Variables. The variables section (at the top) contains filenames, and other information that you will use in the commands that build and manage your project. In this homework, we have specified exactly what variables you need in the Requirements table.

Rules. A rule consists of a target, ≥0 prerequisites, and ≥0 actions.

There are two types of rules.

  1. File rules tell make how to generate a single file by executing ≥1 actions (bash commands). The name of a file rule is always the filename that it is supposed to generate. For example, you could use a file rule called test_mintf to generate the executable (e.g., test_mintf) for that project by compiling the code (e.g., gcc mintf.c test_mintf.c -o test_mintf). In more complex projects, there are often hundreds or thousands of files to be compiled or otherwise generated before the final executable is created. This is what make was originally designed for.
  2. Phony (command) rules let you define a sequence of ≥1 actions (bash commands) that can be run by make as a convenience. Essentially, phony (command) rules are like scripts for automating and simplifying routine commands that developers need to run while working on a project. The name of a phony (command) rule should be a word (usually a verb) that describes what happens when you run those actions. For example, in HW07, you will create a phony (command) rule called submit that submits your code by calling 264submit along with all of the arguments that it requires.

Targets. The target of a rule is just the name of the rule. For file rules, the target will be the name of the file to be generated (e.g., test_mintf). For phony (command) rules, the target will be a word (usually a verb) that describes what the actions in that rule do (e.g., submit, test, etc.).

While most rules (in production code) generate a single file (e.g., an executable), some rules are essentially just commands to do something useful, and are not intended to generate a particular file. For example, in HW07, you will create rules to test your code, check for inadequate tests, submit your code, and run the pretester. These are known as phony (command) rules. They will be declared in the .PHONY target at the bottom of the file. They are simply listed on that one line, separated by spaces.

Prerequisites. The prerequisites… are a list of targets upon which that rule depends. For example, test_count_words depends on count_words.c, count_words.h. If you were to use your new console log macros in your count_words.c, then log_macros.h would also be a prerequisite of test_count_words.

Actions. The actions in each rule consist of commands that could be entered in your shell (bash). Your bash aliases will not work here. That means simply typing gcc would not include the flags we require for the class. Therefore, you will create some variables to ensure that those flags are included whenever you compile. (These are given in the Requirements table below.)

Actions must be indented with a tab character, not four spaces. If you are using Vim with the .vimrc we provided, this is taken care of for you.

The .PHONY target At the end of your Makefile you should have a single rule called .PHONY. Its prerequisites should be the targets for every phony (command) rule in your make file. It should have no actions.

Running make

make   From bash, running make (with no arguments) will run the first (top) rule in your make file.

make target   To run a specific rule, include its name as an argument to make. For example, to run the submit rule, you would type make submit from bash.

When make runs a rule, it first checks its prerequisites.

First, make decides whether to proceed with the current rule at all. If ⓐ the current rule is a file rule, ⓑ the file it refers to (i.e., is supposed to generate) already exists, and ⓒ all prerequites are names of files that exist and are older than the file the current rule refers to, then we stop and do nothing more for the current rule.

Second, if we do need to proceed with the current rule, then make goes through each prerequisite one-by-one. If the prerequisite is the target of any rule in this make file, then make runs that target following this same process. (It is a form of recursion.) Otherwise, if the prerequisite is not a target, then make assumes it is the name of a file that will be needed by the actions in the current rule and simply verifies that it exists; if not, make quits with an error message.

Default target. The first rule in a Makefile is the default. Simply running make (from bash) will build the first target. For example, if your first target in your Makefile is test_count_words, then running make (from bash) would be equivalent to running make test_count_words .

1. Set up your homework directory.

You will use your code from HW06.

Create a directory for HW07.

you@eceprog ~/ $ cd 264
you@eceprog ~/264 $ mkdir hw07
you@eceprog ~/264 $ cd hw07
you@eceprog ~/264/hw07 $

Copy your HW06 code into your HW07 directory.

you@eceprog ~/264/hw07 $ cp ../hw06/* ./
you@eceprog ~/264/hw07 $

The most important file you will create in HW07 is Makefile.

2. Create your Makefile

You will create your Makefile from scratch. However, you will be using it with your miniunit.h from HW06 and log_macros.h from HW05.

Your Makefile will have instructions for how to not only compile a program, but also run your tests, submit, run the pretester, and even run a code coverage tester to search for areas of your code that your tests may not be covering.

A Makefile must be specific to a particular project. For HW07, you will create a Makefile for the count_words project that is included in the starter. For the rest of this semester, you will simply change a few variables at the top of your Makefile to enable it to work for each future assignment.

The specifics on what your Makefile must contain are in the requirements table.

Reminder: You may hand-copy snippets from this homework description (HW07), but do not copy-paste. You won't learn as well, and it won't work anyway.)

Instructions

  1. Create a very simple Makefile to just compile test_count_words
    1. Create a blank file called Makefile.
      you@eceprog ~/264/hw07 $ vim Makefile
    2. Add one rule to compile test_count_words as follows.
      test_count_words: count_words.c test_count_words.c count_words.h miniunit.h log_macros.h gcc -o test_count_words count_words.c test_count_words.c -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic
  2. Test the Makefile from bash.
    1. From bash:
      you@eceprog ~/hw07 $ make
    2. From Vim:
      :make
    Since those commands do not specify a target, the first rule in your Makefile is used.
  3. Modify your Makefile to use the CC variable.
    1. Set the CC variable to gcc at the top of Makefile like this:
      CC=gcc
    2. Replace gcc with $(CC) in your test_count_words rule.
    3. Test that everything works so far.
  4. Modify your Makefile to use the CFLAGS variable.
    1. Set the CFLAGS variable to -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic at the top of Makefile like this:
      CFLAGS=-g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic
    2. Replace -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic with $(CFLAGS) in your test_count_words rule.
    3. Test that everything works so far.
  5. Modify your Makefile to set and use the other variables.
    1. Set the BASE_NAME variable to count_words at the top of Makefile like this:
      BASE_NAME=count_words
    2. Set the SRC_C variable to count_words.c using $(BASE_NAME) to minimize duplication, like this:
      SRC_C=$(BASE_NAME).c
    3. Set the TEST_C variable to test_count_words.c using $(SRC_C) to minimize duplication, like this:
      TEST_C=test_$(SRC_C)
    4. Add all of the rest of the variables specified in the Requirements table. Wherever possible, use existing variables to minimize duplication.
    5. Use the variables you defined in your test_count_words rule. At this point, there should be no mention of …count_words… anywhere in the rule.
    6. Test that everything works so far.
  6. Add a phony (command) rule to submit your code
    1. Add a rule called submit that submits the assignment. Use the variables you defined. You can add more if needed. Your submit rule does not require any prerequisites. However, if you want make submit to check that all required files are present, then you may optionally add $(SUBMIT_FILES) as a prerequisite. It is your choice.
    2. Add the following line to the bottom of your Makefile: .PHONY: submit
    3. Type make submit in bash or :!make submit in Vim to test your rule for the submit phony target. . You may submit to it to test your Makefile, but will not be scored and will not affect your grade.
  7. Add a phony (command) rule to run your tests.
    1. Add a rule called test. Hand-copy the following code.
      test: $(EXECUTABLE) ./$(EXECUTABLE)
    2. Add test to your .PHONY target.
    3. Type make test in bash or :!make test in Vim to test your rule for the test phony target.
  8. Add a phony (command) rule to run the pretester.
    1. Add a rule called pretest that runs the pretester. It should call your submit rule first (using a prerequisite) and then call 264test. Use the variables you created (including ASG_NICKNAME) in the actions for this rule. Do not enter “HW55”, “count_words.c”, or “test_count_words.c” directly.
    2. Add pretest to your .PHONY target.
    3. Type make pretest in bash or :!make pretest in Vim to test your rule for the pretest phony target. It is a dummy pretester that gives everyone 100%.
  9. Add a phony (command) rule called clean that deletes the executable files (test_count_words and test_count_words_gcov). The command to delete a file(s) is rm files…. When working at the terminal, we have that command set to prompt for confirmation. To ensure that make does not prompt for confirmation, you add the -f flag. Thus, the whole command will be rm -f files….
    make clean should delete the executables and any data files created by the code coverage check (if any). To delete file(s), use rm -f ▒▒▒. The -f tells the rm command not to prompt for confirmation or print an error if no such file exists.
    Be careful not to delete your .c or .h files, or your Makefile.
  10. Add the remaining rules, as specified in the Requirements table. Some additional hints are below. Be sure all rules except for $(EXECUTABLE) are listed in your .PHONY: line.

3. Test coverage checking

Code coverage testing—including statement coverage—can help you find bugs in your code by discovering aspects of the functionality that your tests are not exercising. In this section, you will learn how to use test coverage checking, and integrate that functionality into your Makefile.

We will be using one kind of coverage, called line coverage. It runs your tests and monitors which statements in your implementation code were executed. If any statements were not executed, it typically indicates that either your tests are not exercising all of the functionality. It is also possible that you have some dead code. Either way, it should be rectified.

We will be using a tool called GCOV, which works in conjunction with GCC. To check test coverage using GCOV, use this:

# Compile a special version of executable with coverage checking
gcc -o executable source files... -ftest-coverage -fprofile-arcs

# Run the executable to record coverage statistics
./executable

# Print a coverage summary
gcov -f one source file

Detailed information, such as which lines were executed, can be found by opening the count_words.c.gcov in your editor.

Instructions

  1. Try running GCOV from bash. Later, you will be adding this to your Makefile but it is important to first ensure that it works for you—and that you understand what it does.
    You could do this with the included count_words program, but to show this in context of something more familiar, we will demonstrate using and implementation of HW04.
      
    you@eceprog ~/HW07 $ gcc -o test_mintf_gcov mintf.c test_mintf.c -fprofile-arcs -ftest-coverage
    you@eceprog ~/HW07 $ ./test_mintf_gcov
    ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒
    you@eceprog ~/HW07 $ gcov -f mintf.c
    Function 'mintf' Lines executed:61.29% of 31 Function 'print_integer' Lines executed:75.00% of 8 Function '_print_integer_digit' Lines executed:81.82% of 11 File 'mintf.c' Lines executed:68.00% of 50 Creating 'mintf.c.gcov'
    The example above is what you would see if you have not tested adequately. In this case, only 68% of the lines in count_words.c were executed at all. None of the three functions was tested thoroughly. Hopefully, the stats for your code will report 100% coverage.
    This metric excludes blank lines, comments, and lines containing only braces, so 100% line coverage should be attainable.
  2. Add a rule called coverage to your Makefile to check code coverage. Use the variables you defined (e.g., CC, CFLAGS, etc.) wherever possible. Have the GCC command Hint: It should have two .c files as prerequisites, and three actions. Don't forget to add coverage to the list of phony targets (i.e., .PHONY: submit test ▒▒▒▒▒▒▒▒▒▒▒▒▒).
  3. Type make coverage in bash or :!make coverage in Vim to test your rule for the coverage phony target. Verify that it created five new files: test_count_words_gcov, count_words.c.gcov, count_words.gcda, count_words.gcno, test_count_words.gcda, test_count_words.gcno.
  4. Modify the clean rule in your Makefile to remove the data files created by GCOV. You should use wildcards (*.c.gcov *.gcda *.gcno).
  5. Type make clean in bash or :!make clean in Vim to test your rule for the clean phony target. Verify that the five files created by GCOV (test_count_words_gcov, count_words.c.gcov, count_words.gcda, count_words.gcno, test_count_words.gcda, test_count_words.gcno) were deleted.

Requirements

  1. Your submission must contain each of the following files, as specified:
    file contents
    Makefile makefile
    Variable definitions (top section)
    name value
    ASG_NICKNAME HW55
    BASE_NAME count_words
    EXECUTABLE test_count_words
    EXECUTABLE_GCOV test_count_words_gcov
    TEST_C test_count_words.c
    SRC_C count_words.c
    SRC_H count_words.h log_macros.h miniunit.h
    SUBMIT_FILES count_words.c test_count_words.c log_macros.h miniunit.h Makefile
    SHELL /bin/bash
    CC gcc
    CFLAGS -g -std=c11 -Wall -Wshadow -Wvla -Werror -pedantic
    CFLAGS_GCOV $(CFLAGS) -fprofile-arcs -ftest-coverage
    Rules
    target name action(s)
    $(EXECUTABLE) Compile the executable.
    • Reminder: Use variables in all rules, wherever possible.
    test Run executable (test_count_words).
    submit Submit the assignment using 264submit.
    • Rule will look like this:
      submit: ▒▒▒
          264submit ▒▒▒
      
    • Reminder: Use variables. There should be no raw filenames in this rule.
    pretest Pretest the assignment using 264test.
    • Rule should submit before pretesting.
    • Simply include your submit target as a prerequisite of pretest (i.e., pretest: submit)
    • This rule is simple, so we'll give it to you:
      pretest: submit
          264test $(ASG_NICKNAME)
      
    coverage Check the code coverage of your tests. The executable you create should be called test_count_words_gcov.
    clean Delete all files created by any of the above rules.
    1. Use the variables wherever possible to avoid duplicating information.
      • Information in the variables (above) should not be duplicated elsewhere. ◀◀◀◀◀◀
      • Where possible, define variables in terms of existing variables.
        Ex: SRC_C=$(BASE_NAME).c
      • The strings test_ (lowercase), HW55, count_words, gcc, -Wall, log_macros.h, and miniunit.h should appear only once (each) in your Makefile.
    2. Use prerequisites to reduce duplication and ensure each action has what it needs.
      • A rule should depend (directly or indirectly) on every file it uses.
      • Do not repeat exactly the same compilation command in multiple places.
    3. Do not add pointless (unnecessary) prerequisites.
      • Ex: Any rule that depends on test_count_words should not list count_words.c or test_count_words.c as prerequisites since they are implied by test_count_words and thus unnecessary.
      • Ex: make clean should not depend on anything.
    4. miniunit.h should use log_macros.h to the extent possible—at least use the ANSI_▒▒▒ constants.
    5. make submit should result in:
      264submit HW55 count_words.c test_count_words.c log_macros.h miniunit.h Makefile
    log_macros.h
    (same as HW05)
    miniunit.h
    (same as HW06)
    test_count_words.c
    (same as HW06)
  2. You may hand-copy any code snippets you find in this homework description into your HW07 submission.
    • Do not use copy-paste. You learn more from hand-copying unfamiliar syntax. Expect problems if you ignore this.
    • Adaptation is strongly recommended. Some snippets may not work in your file as is.
    • Be sure you understand what you are copying. Correct functioning of your code is your responsibility.
    • Copying from this page is not necessary. This permission is given as a convenience, since some of the syntax may be unfamiliar, and this homework is more tightly specified than most others.
  3. You may use any of the following:
    header functions/symbols allowed in…
    stdbool.h bool, true, false *.c, *.h
    stdio.h printf, stdout *.c, *.h
    string.h strcmp test_count_words.c, miniunit.h
    stdlib.h EXIT_SUCCESS test_count_words.c
    Check with the instructor if you wish to use others. If you find yourself in need of something else, there's a decent chance you are on the wrong track.
  4. miniunit.h should have #include "log_macros.h" so that users of miniunit.h don't need to include both.
  5. Submissions must meet the code quality standards and the course policies on homework and academic integrity.
  6. Write multi-line macros with one C statement on each line. Do not try to cram many statements on a single line of code. That would not be readable.
  7. Indent your macros similarly to regular C code. Your code must be readable.

Submit

To submit HW07 from within your hw07 directory, type 264submit HW07 Makefile miniunit.h log_macros.h test_count_words.c

Pre-tester

The pre-tester for HW07 has not yet been released. As soon as it is ready, this note will be changed.

Q&A

  1. Shouldn't a Makefile refer to .o files?

    We are using makefiles in a simplified manner that does not require awareness of .o files.
  2. What is HW55? Is that a typo?

    HW55 is a dummy assignment that you will use to test the rules in your Makefile that deal with assignment submission and pretesting. It will not be scored and will not affect your grade.
    HW07 is the “real” assignment you are doing here.
  3. Should make coverage depend on $(EXECUTABLE)?

    No.
  4. Do I call make $(EXECUTABLE) or make test_count_words?

    Call make test_count_words (from bash).
  5. Can my Makefile refer to the ANSI color constants in log_macros.h?

    No. They must be duplicated in Makefile. The make tool cannot read C code directly.

Updates

10/3/2022
  • Clarified the Structure of a Makefile section.
2/22/2022
  • TEST_EXPECTED, TEST_ACTUAL, and EXECUTABLE_DBG are not required.
  • expected.txt must not be included in SUBMIT_FILES.
  • Screenshot was updated to reflect the correct variable names.
  • Requirement about referring to variables wherever possible was clarified by adding the corollary that test_ (lowercase), HW55, count_words, gcc, -Wall, log_macros.h, and miniunit.h should appear only once each in your Makefile.
2/27/2022
  • TEST_EXPECTED and expected.txt should not be referenced anywhere.
3/6/2022
  • Trivial: Reordered variables in Requirements table.