When using a build system for building embedded C applications, we want to be able to automatically track our source file dependencies. This allows us do incremental builds, where each time we build we only build what is necessary based on what has changed. This saves us time each time we build and over the course of a development effort, this accumulated time can be significant.
Rake cannot do this by itself, but we can use GCC to do this for us. You may be familiar with this functionality of GCC if you've used it from a Makefile to generate dependencies.
What we'll do here is use GCC to automatically determine the C-language dependencies of each source file, and then set up our Rakefile to manage and import them correctly.
Prerequisites
- Rake (via Ruby) has been installed and is available on your path. As noted in the comments, Rake version 10.4.2 may be necessary.
- GCC has been installed and is available on your path.
Source
The source used in this article can be found on GitHub at: ElectronVector/blog-rake-gcc-depends.
Sample C Application
Let's define a little C application that we'll use to test our build. This consists of a main.c
file which uses another software module by including module.h
. When run, this application simply prints some text to standard out.
main.c
#include <stdio.h>
#include "module.h"
int main () {
printf("Running main\n");
module_run();
}
module.c
#include <stdio.h>
#include "module.h"
void module_run ()
{
printf("Running module\n");
}
module.h
void module_run ();
Include Tree
The diagram below shows the how the application files are related by include statements. We can see that module.h is included by both main.c and module.c.
Initial Rakefile
In Using Rake to Build a Simple C Application, we created simple Rakefile. One of the primary deficiencies of that example was that it was unable to track C-language dependencies. Here we'll start with a similar Rakefile:
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('*.exe')
source_files = Rake::FileList["*.c"]
object_files = source_files.ext(".o")
task :default => "app.exe"
desc "Build the binary executable"
file "app.exe" => object_files do |task|
sh "gcc #{object_files} -o #{task.name}"
end
rule '.o' => '.c' do |task|
sh "gcc -c #{task.source}"
end
If we run Rake, we can see the application build:
$ rake
gcc -c main.c
gcc -c module.c
gcc main.o module.o -o app.exe
If we touch main.c, we can see that only main.o is built again, as we expect.
$ touch main.c
$ rake
gcc -c main.c
gcc main.o module.o -o app.exe
However if we touch module.h and rebuild, nothing happens:
$ touch module.h
$ rake
This is because we haven't yet defined the any dependencies on module.h. In this application though, if module.h changes we need to rebuild both main.o and module.o. So we need to define the additional dependencies to do the incremental build correctly.
Using GCC to Determine Dependencies
To use the GCC preprocessor to determine the dependencies of a particular c file, use the -MM or -M option. When the rule spans more than one line, then a backslash is placed at the end of the line.
The -MM option does not include any system headers:
$ gcc -MM main.c
main.o: main.c module.h
The -M option does include dependencies on system headers however these are unlikely to change, so the -MM option will be fine for our purposes.
$ gcc -M main.c
main.o: main.c c:\mingw\include\stdio.h c:\mingw\include\_mingw.h \
c:\mingw\lib\gcc\mingw32\4.8.1\include\stddef.h \
c:\mingw\lib\gcc\mingw32\4.8.1\include\stdarg.h \
c:\mingw\include\sys\types.h module.h
By default, a Make-style dependency rule is sent to stdout. To write the dependency rule to a file, use the -MF option:
$ gcc -MM main.c -MF main.mf
Strategy
We'll use GCC to create to create an .mf files containing Make-style dependencies for each .c file. Then we'll use the import
feature of Rake to import these dependencies. We'll do this with Rake's built-in support for loading Make-style dependencies in rake/loaders/makefile
. To direct Rake to use the makefile loader, our dependency files will need to have a .mf extension.
Implementation
Setup
The first thing we want to do is create a FileList of the dependency files we expect to use, just like we do for source and object files.
source_files = Rake::FileList["*.c"]
object_files = source_files.ext(".o")
depend_files = source_files.ext(".mf") # New dependency file list.
Also, we want to update our clean
task to remove these .mf files.
CLEAN.include('*.o', '*.mf')
Dependency File Rule
Then we'll add a rule for making the dependency files:
# The rule for creating dependency files.
rule '.mf' => '.c' do |task|
sh "gcc -MM #{task.source} -MF #{task.name}"
end
Importing
To use the Makefile loader, we need to "require" it by adding this statement to the top of our Rakefile:
require 'rake/loaders/makefile'
Next we'll add an import statement for each dependency file in our list. This is what loads each dependency file. Note that the import
statement loads the dependency file after the Rakefile is loaded, but before and tasks are run. This is what allows us to update the dependency files before compilation.
# Explicitly import each dependency file. If the file doesn't
# exist, the file task to create it is invoked.
depend_files.each do |dep|
puts "importing #{dep}"
import dep
end
Then we add a task to create each dependency file if it doesn't exist.
# Declare an explict file task for each dependency file. This will
# use the rule defined to create .mf files defined earlier. This
# is necessary because it assures that the .mf file exists before
# importing.
depend_files.each do |dep|
file dep
end
Now if we clobber and rebuild, we see that the .mf files are generated as part of the build.
$ rake clobber
...
$ rake
importing main.mf
importing module.mf
gcc -MM main.c -MF main.mf
gcc -MM module.c -MF module.mf
gcc -c main.c
gcc -c module.c
gcc main.o module.o -o app.exe
Now if we touch module.h, the correct files are rebuilt:
$ touch module.h
$ rake
importing main.mf
importing module.mf
gcc -c main.c
gcc -c module.c
gcc main.o module.o -o app.exe
Updating Dependency Files
There is one last issue with this setup. If we were to add an additional "nested_include.h" file and include it in module.h like so:
$ touch nested_include.h
New module.h:
#include "nested_include.h"
void module_run ();
Our new include tree looks like this:
If we rebuild our application, the correct items are rebuilt:
$ rake
importing main.mf
importing module.mf
gcc -c main.c
gcc -c module.c
gcc main.o module.o -o app.exe
However, the dependency files have not been rebuilt. If we take a look at their contents we see that neither lists the new dependency on nested_include.h.
main.mf:
main.o: main.c module.h
module.mf:
module.o: module.c module.h
What's happening here is that the dependency files themselves are dependent on the same files as the source files used to generate them. For example both main.mf and module.mf are dependent upon changes to module.h and need to be regenerated if module.h changes.
To explicitly state these dependencies, we can list them directly in the .mf file, so that main.mf would contain:
main.o: main.c module.h
main.mf: main.c module.h
Then when we import these dependencies into our Rakefile, we'll know when to regenerate each .mf file.
To create the .mf files in this way, we need to update our rule to be:
# The rule for creating dependency files.
rule '.mf' => '.c' do |task|
cmd = "gcc -MM #{task.source}"
puts "#{cmd}"
make_target = `#{cmd}`
open("#{task.name}", 'w') do |f|
f.puts "#{make_target}"
f.puts "#{make_target.sub(".o:", ".mf:")}"
end
end
Here we capture the dependency output from GCC (rather than having it write to a file) and write the file ourselves. In the file we include dependencies for the .o and .mf files.
Now we can return to our previous state by removing the #include "nested_include.h
from module.h, clobbering and rebuilding.
If we then add back in the #include "nested_include.h
to module.h and rebuild, each dependency file is updated as we expect.
$ rake
importing main.mf
importing module.mf
gcc -MM main.c
gcc -MM module.c
gcc -c main.c
gcc -c module.c
gcc main.o module.o -o app.exe
main.mf:
main.o: main.c module.h nested_include.h
main.mf: main.c module.h nested_include.h
module.mf
module.o: module.c module.h nested_include.h
module.mf: module.c module.h nested_include.h
Now we have a system for building C applications that will correctly manage dependencies, allowing us to do incremental builds.