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?
- It’s labor intensive – It takes a while to type all these commands
- It’s error prone – We have to remember a lot of commands and parameters; it’s easy to forget something or make typos
- 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:
- Look for a file named Makefile in the current directory
- Look for a target in the Makefile called sherry_trifle
- The sherry_trifle target exists so determine whether its dependent targets sylabub_topping and spongy_base also exist
- sylabub_topping does not exist so check whether there’s a rule to create sylabub_topping
- There is, check that brandy, lemon_juice and double_cream exist (we’ll assume they do)
- Run whip cream and stir in brandy and lemon juice commands in order to create sylabub_topping
- Repeat steps 4 to 6 for spongy_base
- Now that the dependent targets for sherry_trifle exist run spoon sylabub_topping onto spongy base and our Sherry Trifle has been made.
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:
- You only have to remember short, memorable commands
- You can rapidly build, deploy and test your app
- 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
- 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.