electronvector.com/blog/using-gcc-for-automatic-c-language-dependency-man...

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 * Ruby has been installed and is available on your path. * 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.

ImplementationSetup

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.


Comments (0)

Sign in to post comments.