• Articles
  • Ceedling Book
  • Eclipse Toolkit
  • About
Menu

ElectronVector - Test-First Embedded Software

Better embedded software
  • Articles
  • Ceedling Book
  • Eclipse Toolkit
  • About

Get your free guide: How to use Ceedling for embedded test-driven development

Using GCC for Automatic C-Language Dependency Management with Rake

February 4, 2015

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.

← Escape Your Embedded Vendor's IDEUsing Rake to Build a Simple C Application →
Matt Chernoksy

Matt Chernosky


book-3d.png

Need more help with Ceedling?

A Field Manual for Ceedling is filled with examples for how to write tests (and create mocks) for your embedded software.

Get the Book

Add unit tests to your current project with Ceedling

Try embedded TDD right now with Ceedling

Mocking embedded hardware interfaces with Ceedling and CMock

CMock vs FFF -- A comparison of mocking frameworks


Don't miss a post!

Sign up here and I'll keep you in the loop.

ElectronVector