Building with Rake

Ah, the elegant solution that is Ruby's make tool! Starting with the most basic implementation, it's going to look a lot like our make build, with a slightly different notation. It has a major advantage over the makefiles, however: It can use objects created by CMock or the generator scripts directly without needing to access them through their command line interface.

Separation of Concerns

Because we're using Ruby, we can immediately start to take advantage of our ability to execute code snippets. Instead of cluttering our rakefile up with target-specific junk like compile flags and such, we're going to break that stuff out into a separate file. This file could be another Ruby file, or (as we're going to do now) it could be a standard text file of some type. We've chosen yaml (it's easy for humans to read, and fast to parse).

So let's start our rakefile.rb file by requiring a few common utilities and then loading the settings. We're just going to load the yaml file as a hash called CONFIG. Naming it with a capital letter makes it immune to changes.

require 'rake/clean'
require 'fileutils'
require 'yaml'

CONFIG = YAML.load(File.read('project.yml'))

Next, we're going to do a little trick for our own convenience. We're going to go through the main sections of the yaml file and create a large constant object for each one. Instead of needing to look up a linker setting like CONFIG[:linker][:exe] we will just be able to say LINKER[:exe]. It's a convenient shortcut to make our file more readable:

CONFIG.each_key do |k|
  eval "#{k.to_s.upcase} = CONFIG[:#{k}]"
end

The Project File

Let's pause for a moment and see what we'd like our yaml file to look like. The idea is that we want to be able to specify how each of our tools should be used, along with the paths for our project. Then, we should be able to write a rakefile that is more generic and can be carried from project to project. Ceedling actually works something like this, though it has many many more options than we are going to cover in this article.

---
:path:
  :test:     'test/'
  :src:      'src/'
  :inc:
    - 'inc/'
    - 'targets/gcc_armcortexM3/'
  :aux:      'targets/gcc_armcortexM3/'
  :unity:    '../unity/src/'
  :cmock:    '../cmock/src/'
  :build:    'build/'
  :mocks:    'build/mocks/'
  :runners:  'build/runners/'
  :obj:      'build/objs/'
  :exe:      'build/exe/'
  :results:  'build/results/'
  :coverage: 'build/coverage/'
:compiler:
  :exe: arm-none-eabi-gcc
  :args:
    - '-c'
    - '-g'
    - '-mcpu=cortex-m3'
    - '-mthumb'
    - '-Wall'
    - '-Wno-address'
    #- '-std=c99'
    #- '-pedantic'
    - '-DTEST'
    - '-DUNITY_EXCLUDE_STDINT_H'
    - '-DUNITY_EXCLUDE_LIMITS_H'
    - '-DUNITY_EXCLUDE_SIZEOF'
    - '-DUNITY_INCLUDE_DOUBLE'
    - '-DUNITY_SUPPORT_TEST_CASES'
    - '-DUNITY_INT_WIDTH=32'
    - '-DUNITY_LONG_WIDTH=32'
    - '-DUNITY_INCLUDE_CONFIG_H'
:linker:
  :exe: arm-none-eabi-gcc
  :args:
    - '-lm'
    - '-mcpu=cortex-m3'
    - '-mthumb'
    - '-specs=nosys.specs'
    - '-T targets/gcc_armcortexM3/qemu.ld'
:simulator:
  :exe: qemu-system-arm
  :args:
    - -cpu cortex-m3
    - -M lm3s6965evb
    - -no-reboot
    - -nographic
    - -kernel
:coverage:
  :exe: arm-none-eabi-gcov
  :args:
  - '-b'
:cmock:
  :mock_path: 'build/mocks/'
  :includes_h_pre_orig_header:
  - Defs.h
  - LPC1768.h
  :verbosity: 1
  :plugins:
  - :expect
  - :ignore
  - :array


So, it's not small... but it's fairly intuitive, right? It's obviously organized by a combination of indentation and some light punctuation (primarily colons and hyphens). We've stated where to find all of our paths. We've stated which tools to use and what options to apply to them. With that as our reference, we can continue our rakefile.

Directories

First thing, we need to gather together all our directories and make sure we've created those that are required for a build. All of our directories are entering through our PATH object, so we can start by just making a list of includes.

PATH_INCLUDES = [ PATH[:test], PATH[:src], PATH[:unity],
  PATH[:cmock],PATH[:mocks], PATH[:inc] ].flatten

And we can search through our list of PATHs and make sure we know which ones we're in charge of building if necessary. We do this with a regular expression.

BUILD_PATHS   =  PATH.values.select {|v| v =~ /build(?:\\|\/).*/}

Once we have those build paths, we can add each of them to our list of things to get clobbered when we want to do a clean build:

BUILD_PATHS.each do |f|
  directory f
  CLOBBER.include(f+"/*.*")
end

And we make a task called :directories that we can use as a prerequisite for other tasks to run. That way we'll be sure to have the directories built if needed.

task :directories => BUILD_PATHS

A Test Suite

Let's make the test suite already! We know where the tests are... they're in PATH[:test] which we acquired from our yaml file. So, let's make a test set of all the files that match Test*.c in that path. But it's not the C files we're really after, it's the result files of running those tests! So we do a little replacement magic to create our test list:

testset = Dir.glob(PATH[:test]+'Test*.c').map do |f| 
  f.sub(PATH[:test],PATH[:results]).sub('.c','.testresults')
end

Awesome. Now we can make a task to run it. Since this is going to be a task we expect users to be able to call directly, we also add a convenient description. This description gives them a hint as to what it is for, as well as make sure it will show up in the list of possible tasks if the user types rake -T (the capital T is important).

desc "run all unit tests"
task :test => ([:directories] + testset) do |t|
  summarize(t.prerequisites[1, t.prerequisites.size - 1])
end

That summarize function isn't a built-in rake function. We're going to have to define what it means to summarize the results. But first let's look at the task line. This is how Rake thinks about tasks. It starts with a symbol for the task (in this case :test). If the task has prerequisites, it lists those after the => as an array. In this case, we're combining two arrays to make sure we get all the directories built AND all the parts of our test suite. Our task COULD have stopped here, but we also want it to summarize the results. Which brings us to where we started. What does summarize look like?

def summarize(filename)
  require "../unity/auto/unity_test_summary.rb"
  @summarizer ||= UnityTestSummary.new
  @summarizer.set_targets(filename)
  puts @summarizer.run
end

Well that is nice. It's a simple function which is mostly just pulling in the existing unity test summarizer and passing our data to it. So once all the prerequisites have been met, it's going to make a nice summary output for us.

The Basic Prerequisites

Easier said than done, right? Well, this is where things look a lot like a standard makefile. We define a number of rules. These rules are mostly pretty intuitive (Or at least their meaning. The notation isn't so clear).

The rule for building a results file is that it depends on a test executable being present for that module. If that's true, it runs it.

rule /build\/results\/\w+\.testresults/ => lambda { |fn| prereq(fn, [PATH[:exe]+'BASE.exe']) } do |t|
  exec_cmd = SIMULATOR[:exe] + " #{SIMULATOR[:args].join(' ')} #{t.source}"
  results = execute(exec_cmd, false)
  File.open(t.name, 'w') {|f| f << results }
end

So wow. That's ugly, right? Let's pick it apart. First we declare that it's a rule. The rule is a regular expression which matches anything in our results folder that ends in testresults. The prerequisite for this is calculated on the fly using the lambda expression, which uses the results filename to determine what the executable filename would be. Then, once inside the task, we look up how to run our simulator and do that. We collect the output from the simulator and we create a results file for it.

That's actually one of the more complicated rules. We have rules for making those executables:

rule /build\/exe\/Test\w+\.exe/ => lambda { |fn| prereq_search(fn) } do |t|
  linkit(t.name, t.prerequisites)
end

And rules for building various object files:

rule /build\/objs\/\w+\.o/ => lambda { |fn| prereq(fn, [PATH[:src]+'BASE.c']) } do |t|
  compile(t.name, t.source)
end

rule /build\/objs\/Test\w+Runner\.o/ => lambda { |fn| prereq(fn, [PATH[:runners]+'BASE.c']) } do |t|
  compile(t.name, t.source)
end

rule /build\/objs\/Test\w+\.o/ => lambda { |fn| prereq(fn, [PATH[:test]+'BASE.c']) } do |t|
  compile(t.name, t.source)
end

rule /build\/objs\/Mock\w+\.o/ => lambda { |fn| prereq(fn, [PATH[:mocks]+'BASE.c']) } do |t|
  compile(t.name, t.source)
end

file PATH[:obj]+'unity.o' do |t|
  compile(t.name, PATH[:unity]+'unity.c')
end

file PATH[:obj]+'cmock.o' do |t|
  compile(t.name, PATH[:cmock]+'cmock.c')
end

file PATH[:obj]+'startup.o' do |t|
  compile(t.name, PATH[:aux]+'startup.c')
end

file PATH[:obj]+'uart.o' do |t|
  compile(t.name, PATH[:aux]+'uart.c')
end

At this point you could actually build a basic test except that we've left a couple of important functions unexplained.

Determining Prerequisites

Many of the tasks above use a function called prereq in their lambda expression. This isn't a built-in function either. It's a function which is serving as shorthand for the more complicated tasks we're doing here. It looks something like this:

def prereq(infile, outfiles, remove=nil)
  base = File.basename(infile).split(/\./)[0]
  base.sub!(remove,'') unless remove.nil?
  outfiles.map!{ |f| f.sub("BASE",base) }
end

This function does a quick extension replacement on the name of the passed file to give a filename that represents the prerequisite. This is a victory for consistent naming and definitely makes life easier. 

The more interesting function, however, is the one called by the lambda expression for the link step. It's this monstrously large hunk of code:

def prereq_search(infile)
  base = File.basename(infile,'.exe')
  srcfile = PATH[:test]+base+'.c'
  outfiles = File.readlines(srcfile).keep_if do |s| 
    s =~ /^\#include\s*\"\w+\.h\"/
  end.map do |s| 
    s.match(/^\#include\s*\"(\w+)\.h\"/)[1]
  end
  used_mocks = false
  outfiles.map! do |s|
    if (/^(?:Mock|unity|cmock)/.match(s) || 
        File.exists?(PATH[:src]+s+'.c')  || 
        File.exists?(PATH[:aux]+s+'.c'))
      PATH[:obj]+s+'.o'
    else
      puts "cannot find #{s+'.c'}" if $verbose
      nil
    end
  end.compact!
  outfiles += [PATH[:obj]+base+'.o', PATH[:obj]+base+'Runner.o'] 
  outfiles += [PATH[:obj]+"cmock.o"] 
  outfiles += [PATH[:obj]+"uart.o", PATH[:obj]+"startup.o"]
  outfiles
end

Wow. What's that all about? Well, without getting into the nitty gritty, this function is in charge of opening your test file and reading out all the #include lines. Once it has that list, it sorts them. If they start with the word Mock, then they're obviously supposed to be created on the fly. So it can ignore those. Everything else, it looks up and checks if a corresponding C file exists for it. It keeps all the mocks and the items with C files in the list, and throws everything else away. Then, it adds some consistent items, like a test runner (which should automatically be generated), the unity and CMock source files, and finally any simulator-specific files that are required. THAT LIST becomes the prerequisites for linking an executable.

Tools

Now that we know where it came up with all the prerequisites and tasks, we're ready to look at where it pulls in the rest of the tools. In the tasks declared above, we have the obvious compile and link functions. These are defined much as you would expect (by making external calls to the tools):

def compile(outfile, srcfile)
  includes = PATH_INCLUDES.map{|f| "-I#{f}"}.join(' ')
  execute(COMPILER[:exe] + 
          " -o #{outfile} #{srcfile} #{includes} #{COMPILER[:args].join(' ')}")
end

def linkit(outfile, srcfiles)
  execute(LINKER[:exe] + 
          " -o #{outfile} #{LINKER[:args].join(' ')} #{srcfiles.join(' ')}")
end

def execute(cmd, raise_on_error=true)
  response = `#{cmd}`
  exitstatus = $?.exitstatus
  raise "ERROR #{exitstatus.to_s} #{response}" if ((exitstatus != 0) && raise_on_error)
  response
end

Test Runners

Ok. So we have rules and tools for doing most things now... but if you were to run this on a project, it would complain that it can't find your test runner C file. Isn't it supposed to automatically make this? Well yes... but we need to TELL it to do that. So we need to add another rule:

rule /build\/runners\/Test\w+Runner\.c/ => lambda { |fn| prereq(fn, [PATH[:test]+'BASE.c'], /Runner/) } do |t|
  require "../unity/auto/generate_test_runner.rb"
  @runner_generator ||= UnityTestRunnerGenerator.new()
  @runner_generator.run(t.source, t.name)
end

Much like the other rules, this one automatically deduces the name of the Test file that the Runner depends on. It then pulls in our convenient test runner generator script from Unity's auto directory and runs it. It's worth noting here that when we call "require" in Ruby, it only pulls in the library the first time. Any other calls get it for free.

Mocks

Mock objects have exactly the same issue as the runner, and almost an identical solution. here we will pull in the CMock library after deducing the header file based on the name of the Mock:


rule /build\/mocks\/Mock\w+\.c/ => lambda { |fn| prereq(fn, [PATH[:src]+'BASE.h'], /Mock/) } do |t|
  require "../cmock/lib/cmock.rb"
  @cmock ||= CMock.new(CMOCK)
  @cmock.setup_mocks(t.source)
end

Conveniences

At this point, our rakefile is full functional. We can type "rake test" and we will compile, link, execute, and summarize all the unit tests. As the codebase begins to grow, though, this will become a process that might take a long time. Delays are the enemy of a good TDD flow, so let's add some convenience tasks for running a particular unit test:

namespace :test do
  task :unit => [:test]
  task :all => [:test]

  Dir.glob(PATH[:test]+'Test*.c').each do |f|
    basename = File.basename(f,'.c').gsub(/^(?:.*[\\\/])?Test/,'')
    desc "run unit test for #{ basename }"
    task basename => [:directories, PATH[:results]+'Test'+basename+'.testresults'] 
      { |t| summarize(t.prerequisites[1, t.prerequisites.size - 1]) }
    task basename+".c" => [ basename ]
    task "Test"+basename+".c"  => [ basename ]
  end
end

This little hunk of code first creates a couple aliases for "rake test". The user may now write "rake test:all" or "rake test:unit" and it will do the same thing. 

Next, it looks in the test directory and creates a set of tasks for EACH of the test files it finds there. It add a description for the shortest version of the name, then common aliases. So, if you have a test file named TestMyFile.c, you can now run it by typing any of the following:

  • rake test:MyFile
  • rake test:MyFile.c
  • rake test:TestMyFile.c

The first is the one you will most likely type all the time. It's shortest an convenient. The others are there as a convenience to you if you attempt to integrate your rakefile into an IDE. Many IDE's can trigger an external tool, passing the current file as an argument. If yours does this, you can trigger a build by pressing a hotkey when you are looking at either the test file itself or its related source file. How convenient is that?

Finishing Touches

At this point, everything should be working. Very likely, if you're like me, you'll continue to tweak this file over time. You'll add verbosity control. You'll have it tell you what it's doing all the time. You might add coverage reporting. You're working in Ruby, so the sky is the limit, really... eventually, you might have your own homebrew version of Ceedling... or you could just head on over to the Ceedling page.

Happy Testing!