DEVELOPERS BLOG

Command Line Rocks! Part 3 – Make and Makefiles

So far in the Command Line Rocks series we’ve covered building from the command line in part 1 and improving our project structure and using libraries in part 2.

Hopefully you’re getting a feel for the awesome power of the command line. Not only do you gain a better understanding of the underlying program creation processes, you also gain more control and flexibility than when using an IDE.

In this post, we’re going to create a streamlined build process using Make and Makefiles. Get ready for super quick build/deploy/test cycles. Oh and you’ll learn how to make a Sherry Trifle!

A FEW PROBLEMS

From the previous two articles we should have the basics of setting up a project and building from the command line. Unfortunately, our build process has a few problems.

Here are the commands we need to execute in order to compile our source code, package it into a BAR file and deploy that BAR file to the BlackBerry 10 simulator:

qcc -Vgcc_ntox86 -Iinclude -c src/prank_notifier.c -o build/prank_notifier.o qcc -Vgcc_ntox86 -Iinclude -c src/main.c -o build/main.o qcc -Vgcc_ntox86 -lbps build/prank_notifier.o build/main.o -o build/main blackberry-nativepackager -package build/GoodbyeIDE.bar bar-descriptor.xml blackberry-deploy -installApp 192.168.2.128 build/GoodbyeIDE.bar

What’s wrong with our current build process?

  1. It’s labor intensive – It takes a while to type all these commands
  2. It’s error prone – We have to remember a lot of commands and parameters; it’s easy to forget something or make  typos
  3. It’s wasteful – Everything is rebuilt irrespective of what has changed. If we only change main.c, there’s no point in rebuilding prank_notifier.o

LET’S MAKE THINGS BETTER

make is a powerful tool that can be used to build virtually anything in software. It’s well documented, has been around for decades and is still widely used today (the Linux kernel is built using make). Both QNX and Qt provide tools that help you to build projects using make, so it makes sense for us to use it for our project.

RULES

make works by reading rules from Makefiles. These rules tell make how to build the different parts of our project.

The format of each rule within a Makefile is:

<target filename> : <dependent targets>    <command(s) to create the target (also known as a recipe)>

A Makefile is similar to a cooking recipe. It contains a list of ingredients and instructions on how to combine them. Let’s look at an example featuring one of my favourite desserts: a Sherry Trifle.

A Makefile for a Sherry Trifle

A Sherry Trifle has two main parts:

  • Sylabub topping – Made from brandy, lemon juice and double cream
  • Fruity, spongy base – Made from sponge, brandy, blackberries and jam

The two parts are made individually and then combined. A Makefile for a Sherry Trifle would look something like this:

sylabub_topping: brandy lemon_juice double_cream      whip cream      stir in brandy and lemon juice spongy_base: sponge sherry blackberries jam      soak sponge with sherry      add blackberries and jam sherry_trifle: sylabub_topping spongy_base      spoon sylabub_topping onto spongy base

How does make process the Makefile?

To “make” our trifle we’d need to run the following command:

make sherry_trifle

Here’s how make will process this command:

  1. Look for a file named Makefile in the current directory
  2. Look for a target in the Makefile called sherry_trifle
  3. The sherry_trifle target exists so determine whether its dependent targets sylabub_topping and spongy_base also exist
  4. sylabub_topping does not exist so check whether there’s a rule to create sylabub_topping
  5. There is, check that brandy, lemon_juice and double_cream exist (we’ll assume they do)
  6. Run whip cream and stir in brandy and lemon juice commands in order to create sylabub_topping
  7. Repeat steps 4 to 6 for spongy_base
  8. Now that the dependent targets for sherry_trifle exist run spoon sylabub_topping onto spongy base and our Sherry Trifle has been made.
The finished trifle, image courtesy of Christine Rondeau

The finished trifle, image courtesy of Christine Rondeau

OUR PROJECT MAKEFILE

Now we’re ready to create a Makefile for our project. Let’s start by examining our dependency tree:

Dependency Tree

  • prank_notifier.o depends on:
    • prank_notifier.h
    • prank_notifier.c
  • main.o depends on:
    • prank_notifier.h
    • main.c
  • main depends on:
    • prank_notifier.o
    • main.o
    • The bps library
  • GoodbyeIDE.bar depends on:
    • main
    • bar_descriptor.xml

Creating our Makefile

For each one of the generated files in our dependency tree we need a rule. Start by creating a file called Makefile in $PROJECT_DIR/build.

Add the following rules to Makefile. Note we have to update the paths to source files so that they can be found from the build folder:

prank_notifier.o : ../include/prank_notifier.h ../src/prank_notifier.c         qcc -Vgcc_ntox86 -I../include -c ../src/prank_notifier.c -o prank_notifier.o main.o : ../include/prank_notifier.h ../src/main.c         qcc -Vgcc_ntox86 -I../include -c ../src/main.c -o main.o main : prank_notifier.o main.o         qcc -Vgcc_ntox86 -lbps prank_notifier.o main.o -o main

Now delete all files except Makefile from $PROJECT_DIR/build and run:

make main

Our main binary should be created along with the .o files created during the build process. On subsequent runs make will output `main’ is up to date because none of the dependent files have changed and main exists. If you change prank_notifier.h, a complete build will run because it is at the top of the dependency tree.

So now we have a command for creating our app binary that is easier to remember and only invokes the compiler when necessary.

You might be thinking that was a lot of code for little gain, and there’s a lot of duplication in the rules (why do we have to specify the paths to the source and include folders so many times?), and you’d be right! Not to worry, we’ll be making this a whole lot more efficient later on.

Default rule

make can be executed with no arguments, in which case the first rule in the Makefile will be processed. Currently this rule is prank_notifer.o. It would be more useful if our default rule was the one for main. Move the main rule to the top of the Makefile and run:

make

The main rule should be processed. It is good practice to set the default rule as the rule that will be executed most often.

Add a rule for packaging

Now let’s create a rule for creating our BAR file by adding the following to the Makefile:

GoodbyeIDE.bar : main             blackberry-nativepackager -package GoodbyeIDE.bar ../bar-descriptor.xml -devMode

Now we can create our BAR file by running:

make GoodbyeIDE.bar

This is OK, but we still need to remember the name of our project in order to package it. We can make things easier still by adding the following rule:

package : GoodbyeIDE.bar

Now when you run make package the recipe for creating GoodbyeIDE.bar will be executed. Nice.

Phony targets

BUT wait! There’s a problem with this new rule. Remember that make will check to see if the target filename exists before executing a recipe. If we create a file named package, make package will not do anything!

Luckily there’s a simple solution: Use a phony target.

Phony targets are used to tell make that the target of a rule is not a real file, and it should not bother checking for its existence before executing the recipe.

Add the following line above our package rule:

.PHONY : package

Now we can run make package safely  with the knowledge that it will always run the way we want it to.

NOTE: It’s good practice to put the .PHONY keyword immediately above the associated rule so that other people can easily see that the rule has a phony target.

Phony targets are useful in operations where files are moved, copied, or deleted, but not created. Common examples include clean up, install, and deployment.

Cleaning Up

Often we’ll want to start a build from scratch. Instead of deleting all of our build files manually we can add another rule:

.PHONY : clean clean :             rm *.o main GoodbyeIDE.bar

Now to perform a clean up we do:

make clean

Add a rule for deployment

Now let’s add a rule to deploy our app to the simulator, we’ll include the blackberry-deploy command flag -launchApp to auto launch the app once it’s deployed:

.PHONY : deploy deploy : GoodbyeIDE.bar               blackberry-deploy -installApp -launchApp 192.168.2.128 GoodbyeIDE.bar

Now run:

make clean make deploy

The app will be built from scratch, packaged, deployed and launched, all with a single command – pretty cool huh? No more long commands to remember.

Also you’ll find that (for now) the command line tools are much faster than using the Momentics IDE. Depending on the spec of your machine you should see this process take between 2-10 seconds.

Target Summary

We now have the following commands at our fingertips:

  • make – Creates the main binary
  • make package – Does the above and packages main into GoodbyeIDE.bar using bar_descriptor.xml
  • make deploy – Does all of the above and installs GoodbyeIDE.bar onto the simulator, running it automatically
  • make clean – Removes all files generated in the build process

REFACTORING OUR MAKEFILE

You might have noticed that our Makefile includes a lot of repetition. Repetition is always a bad thing. Since make was designed for building projects exactly like ours, you won’t be surprised to learn that it has many tricks up its sleeve to help us write clean, efficient Makefiles.

Using implicit rules

If make encounters a target that doesn’t have a rule, it will attempt to create this target automatically using an implicit rule. To illustrate this, consider the following commands executed in an empty directory:

touch hello.c make hello.o           cc    -c -o hello.o hello.c

We don’t have any rules for make (since we haven’t created a Makefile) so make uses an implicit rule to create hello.o. It knows that .o (object) files can be created from .c (C source) files so looks for a .c file with the same base name, in this case hello.c. Once found, it uses the default compiler command cc and default compiler flags -c -o to create hello.o.

Implicit rules allow us to avoid writing rules for targets that can be created using standard conventions (such as the example above). In our project the rules for main.o and prank_notifier.o can both be replaced with implicit rules.

The only issue we have with the default behaviour above is that the compiler command and flags are wrong. If only there were some way we could override them…

Using predefined variables

make has a number of predefined variables that can be used to configure the implicit rules. The ones we need to configure are:

Variable name            Description                        Our value                 What it does                                                             
CC C compiler command ntox86-gcc Specifies that the QNX Neutrino x86 C compiler should be used
CFLAGS Flags supplied to the C compiler -I ../include Specifies that the C compiler should search “../include” for header files
LDFLAGS Flags supplied to the linker -lbps Links the object files against the bps library
VPATH Search path for target dependencies ../src ../include Specifies the paths which make will search to find dependent files

Since we’re defining variables, we can also define some of our own. This is as simple as VARIABLE_NAME=VALUE and the value is referenced using $(VARIABLE_NAME).Defining our own variables

Let’s define the variables for our BAR filename and the simulator IP address (usually 192.168.X.X):

SIMULATOR_IP=192.168.2.129 BAR_NAME=GoodbyeIDE.bar

We can now use these variables in our various rules.

Better Makefile

Using the principles above, we can now create a new Makefile with the following contents:

VPATH = ../src ../include CFLAGS = -I ../include CC = ntox86-gcc LDFLAGS = -lbps SIMULATOR_IP=192.168.2.129 BAR_NAME=GoodbyeIDE.bar main : main.o prank_notifier.o $(BAR_NAME) : main ../bar-descriptor.xml                blackberry-nativepackager -package $(BAR_NAME) ../bar-descriptor.xml -devMode .PHONY : package package : $(BAR_NAME) .PHONY : clean clean :               rm main *.o $(BAR_NAME) .PHONY : deploy deploy : $(BAR_NAME)              blackberry-deploy -installApp -launchApp $(SIMULATOR_IP) $(BAR_NAME)

SUMMARY

Using the make build system has many benefits, including:

  1. You only have to remember short, memorable commands
  2. You can rapidly build, deploy and test your app
  3. You can add source files and the objects will be built automatically, all you have to do is update the dependencies of main within Makefile
  4. You have a flexible, scalable and efficient build process which can be customized as your project grows

Thanks for taking the time to read through this rather lengthy article! The next article will focus on multiple build configurations, so we can build easily for the simulator and physical BlackBerry devices.

About doturner