Section 3: C Build and Test Frameworks
In the previous discussion section, you learned how to explicitly compile
and run C programs from the command line. You learned how to use the GNU
C Compiler (gcc) to compile both a single-file program, as well as a multi-file program that calculated the square
of an integer. You probably noticed that it can be tedious to have to
carefully enter the correct commands on the command line.
In this discussion section, we will more closely exam the GNU Make build framework
and CMake build framework to automate the C build process.
1. Logging Into ecelinux with VS Code
Once again, we will be using VS Code to log into the ecelinux servers:
- Start VS Code
- Use View > Command Palette to execute Remote-SSH: Connect Current Window to Host...
- Enter
netid@ecelinux.ece.cornell.edu - Use View > Explorer to open folder on
ecelinux - Use View > Terminal to open terminal on
ecelinux
Now clone the GitHub repo using the following commands:
$ git clone --branch sec03 git@github.com:cornell-ece2400/ece2400-sec02-2025 ece2400-sec03
$ cd ece2400-sec03
$ tree
The directory includes the following files:
avg-main.c: source and main for single-fileavgprogramsquare.h: header file for thesquarefunctionsquare.c: source file for thesquarefunctionsquare-adhoc.c: test driver forsquarefunction
2. Using Makefiles to Compile C Programs
Let's remind ourselves how to explicitly compile and run a single-file C program on the command line:
$ gcc -Wall -o avg-main src/avg-main.c
$ ./avg-main
Let's now remove the binary so we are back to a clean directory:
$ rm -r avg-main
We will start by using a new tool called GNU Make which was specifically
designed to help automate the process of building C programs. The key to
using make is developing a Makefile. A Makefile is a plain text
file which contains a list of rules which together specify how to
execute commands to accomplish some task. Each rule has the following
syntax:
target : prerequisite0 prerequisite1 prerequisite2
<TAB>command
A rule show specify how to generate the target file using the list of
prerequisite files combined with a shell command. make is smart enough to
know it should re-make the target if any of the prerequisites change, and
it also knows that if one of the prerequisites does not exist, then it
should try to build it first. This process occurs recursively. It
is very important to note that make requires commands in a rule to
start with a real TAB character. So you should not type the letters
<TAB>, but you should instead press the TAB key and verify that it has
inserted a real TAB character (i.e., if you move the left/right arrows
the cursor should jump back and forth across the TAB). This is the only
time in the course where you should use a real TAB character as opposed
to spaces.
Let's create a simple Makefile to compile a single-file C program. Use
VS Code to create a file named Makefile with the following content:
avg-main: src/avg-main.c
<TAB>gcc -Wall -o avg-main $^
clean:
<TAB>rm -rf avg-main
In this case $^ refers to src/avg-main.c.
See GNU Automatic Variables for more info.
We can use the newly created Makefile like this:
$ make avg-main
$ ./avg-main
make will by default use the Makefile in the current directory.
make takes a command line argument specifying what you want "make". make will look
at all of the rules in the Makefile to find a rule that specifies how
to make the avg-main executable. It will then check to make sure the
prerequisites exist and that they are up-to-date. If that condition is met, it will run
the command specified in the rule for avg-main. In this case, that
command is gcc. make will output to the terminal every command it
runs, so you should see it output the command line which uses gcc to
generate the avg-main executable.
Try running make again:
$ make avg-main
$ ./avg-main
make detects that the prerequisite (i.e., src/avg-main.c) has not
changed and so it does not recompile the executable. Now let's try making
a change in the avg-main.c source file. Modify the printf statement
as follows:
printf("avg( %d, %d ) == %d\n", a, b, c );
You can recompile and re-execute the program like this:
$ make avg-main
$ ./avg-main
In this case, make detects that the prerequisite has changed, and hence
recompile the executable. The ability to automatically
track dependencies and recompile only what is necessary is the key benefit
of using a tool like GNU Make. Makefiles can also include targets which
are not actually files. Our example Makefile includes a clean target
which will delete any generated executables. Let's clean up our directory
like this:
$ ls
$ make clean
$ ls
3. Using CMake to Generate Makefiles for Compiling C Programs
While using make can help automate the build process, the corresponding
Makefiles can grow to be incredibly complicated. Creating and
maintaining these Makefiles can involve significant effort. It can be
particularly challenging to ensure all of the dependencies between the
various source and header files are always correctly captured in the
Makefile.
New tools have been developed to help automate the process of managing
Makefiles (which in turn automate the build process). Automation is
the key to effective software development methodologies. In this course,
we will be using CMake as a key step in our build framework. CMake takes
as input a simple CMakeLists.txt file and generates a more sophisticated
Makefile to use.
Before getting started let's remove any files we have generated and also
remove the Makefile we developed in the previous section.
$ make clean
$ rm -f Makefile
Let's inspect the provided CMakeLists.txt that can be used to generate a
Makefile which will in turn be used to compile a single-file C program:
$ cat CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project(sec02 C)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
include_directories(include)
add_subdirectory(src)
Line 1 specifies the CMake version we are assuming, and line 2 specifies
that we will be using CMake with a C project called sec02.
Line 3 states that we will be generating the database file, so that clangd can work.
The last two lines tell CMake where our code and header files live (src/ and include/, respectively).
Notice, that there is another required CMakeLists.txt inside src/ with more information about which source files to use:
$ cat src/CMakeLists.txt
add_library(sec02-lib
square.c
)
add_executable(square-adhoc square-adhoc.c)
target_link_libraries(square-adhoc PUBLIC
sec02-lib
)
add_executable(avg-main avg-main.c)
Now, let's use CMake to generate a GNU Makefile.
$ cmake .
$ ls
$ less Makefile
NOTE: THERE IS A DOT AFTER cmake! The cmake command will by default
use the CMakeLists.txt in the directory given as a command line
argument. CMake takes care of figuring out what C compilers are available
and then generating the Makefile appropriately. You can see that CMake
has automatically generated a pretty sophisticated Makefile. Let's go
ahead and use this Makefile to build avg-main.
$ make avg-main
$ ./avg-main
CMake will automatically create some useful targets like clean.
$ make clean
With some practice, writing a CMakeLists.txt is simpler than writing a Makefile,
especially when we start working with a large codebase.