In this example we'll set up a simple build system for a C application to be complied with gcc. Using a similar method, we could use a cross-compliler (gcc or otherwise) to compile for whatever embedded target we would like.
Source
The source code in this example is available on github at: ElectronVector/blog-rake-c-simple
Prerequisites
- gcc is installed and available on the path.
- Rake has been installed (see this previous post for help).
The Application
Our application is simply a hello world application consisting only of main.c which contains:
#include <stdio.h>
int main () {
printf("Hi.");
}
The Build Process
Our build process takes place in two steps. First main.c is compiled to produce the object file main.o, and then main.o is linked to build the application binary.
If there were other c files in this application, each would be compiled to its own object file, and then all the object files would be linked in the same operation to produce a single binary.
Building the Rakefile
Simple Tasks
We start with a Rakefile that defines two simple tasks.
task :binary => 'main.o' do sh "gcc main.o -o app.exe" end task 'main.o' do sh "gcc -c main.c" end task :default => :binary
Here, we've defined a binary
task which does the linking to produce the application binary. In the body of the task, we use the sh
command to execute gcc at the shell.
The first binary
task is dependent on the second task named main.o
(this is the meaning of the => 'main.o'
int the task definition. The main.o
task is a "file" task. In the body of this task, we call gcc to generate main.o from main.c. The main.o
task has no dependencies.
Finally, we define the default
task, to be run when no specific task is provided to Rake at the command line. The :binary
task is defined as a dependency of the :default
task so that when the default task is run, the binary task is run.
So when we run rake, first it tries to execute the binary task, but it looks and sees that there is a dependency on the main.o
task, so it runs that first. The binary is built second.
$ rake
gcc -c main.c
gcc main.o -o app.exe
We can then run the application.
$ app.exe
Hi.
Rules and FileLists
Unfortunately, the Rakefile that we have built so far really isn't very good. It works only in the case where we have a single main.c file. If we added another file, it wouldn't be built. What we'd really like is to automatically scan for and compile all c files to object files and then pass them all to the link step to produce the binary.
To do this, we'll use rules and FileLists. We use a rule to tell Rake how to create a .o file from any .c file. Then we'll use a FilelList to tell it which files we want to generate .o files for (hint: it'll be all .c files in the current folder).
Here's a rule for compiling a .c file to a .o file. This is going to replace our original task for building main.o.
rule '.o' => '.c' do |task|
sh "gcc -c #{task.source}"
end
This rule states how to call gcc generically to do this compilation. A new concept here is the task
parameter to the rake task. This is the object representing the task itself. This allows us to reference task.source
, which is the name of the "source" file that this rule is dependent upon. For producing main.o, the task.source
is main.c. Other attributes of the Rake task object can be found in the Rake documentation for Rake::Task.
The other new thing we're doing here is using string interpolation. This is the #{ ... }
syntax within the sh
string to execute. This syntax allows the Ruby code within the braces to be evaluated and then the resulting string is inserted into the enclosing string.
Now that we have a generic rule for compiling .c files, we need to tell Rake what files to compile. For this we can use a FileList with a glob pattern to match all c files in the current folder:
source_files = Rake::FileList["*.c"]
This creates a FileList variable named source_files
which contains all of the .c files in current folder.
We can then create a list of all of the desired object files by using the ext
method of the FileList. This creates a new FileList object_files
by changing the extension of all of the files in the original list:
object_files = source_files.ext(".o")
Now, we can change our binary
task to depend on object_files
, and not simply main.o:
task :binary => object_files do
sh "gcc #{object_files} -o app.exe"
end
So at this point, the complete Rakefile looks like:
source_files = Rake::FileList["*.c"]
object_files = source_files.ext(".o")
task :binary => object_files do
sh "gcc #{object_files} -o app.exe"
end
rule '.o' => '.c' do |task|
sh "gcc -c #{task.source}"
end
task :default => :binary
Clean and Clobber
We also want to be able to clean things up by removing files which have been produced as part of the build. In the C world, this usually means a single operation clean which removes all files. By convention, Rake splits this into two operations: clean and clobber. When we clean we remove all "intermediate" files the were produced along the way (i.e. our object files). When we clobber we remove the intermediate files as well as any final output files (i.e. our binary). In this context, the files removed during a clean is a subset of those removed during a clobber.
Rake provides a simple way of implementing these tasks. Simply require the rake/clean
module by putting this statement at the top of the file:
require 'rake/clean'
Then we can specify the files to remove with each operation:
CLEAN.include('*.o')
CLOBBER.include('*.exe')
Task Descriptions
To keep things organized, we can add a desc
line before each task definition to provide a text description of the task purpose. These are displayed when running rake with the -T option, which lists available tasks provided by the Rakefile.
For example:
desc "Build the binary executable"
task :binary => object_files do
sh "gcc #{object_files} -o app.exe"
end
Summary
We've explored the basics for compiling a C application with Rake, by creating a simple Rakefile. This Rakefile will let us compile a C application from all the .c files in the project folder. Here is our final Rakefile:
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('*.exe')
source_files = Rake::FileList["*.c"]
object_files = source_files.ext(".o")
desc "Build the binary executable"
task :binary => object_files do
sh "gcc #{object_files} -o app.exe"
end
rule '.o' => '.c' do |task|
sh "gcc -c #{task.source}"
end
desc "rake binary"
task :default => :binary
Note that we aren't quite ready to use this in a real project. This implementation is limited by the fact that it doesn't handle C language dependencies. That is, if a particular C file is dependent on another C file, Rake has no way to know about it. It is possible to do this however -- by using gcc to determine the dependencies for us. We'll tackle this in a future post.