A single application

Suppose we have a product (we'll use the archiver, lha for this example) that we'd like to make available on all the processors that the QNX Neutrino RTOS supports. Unfortunately, we've been using our own custom Makefiles for gcc on x86_64, and we have no idea how to make binaries for other processors.

The QNX Neutrino Makefile system makes it very easy for us to build different processor versions. Instead of writing the entire complicated Makefile ourselves, we simply include various QNX Neutrino Makefile system files that will do most of the work for us. We just have to make sure the correct variables are defined, and variants are created.

First, let's get the source code for lha from http://www2m.biglobe.ne.jp/~dolphin/lha/prog/lha-114i.tar.gz and unarchive it:

tar -zxvf lha-114i.tar.gz

This creates a directory called lha-114i. If we run make here, everything will compile, and when it's done, we'll have an x86_64 binary called lha in the src directory.

A typical compile command for this application using the original Makefile looks like:

gcc -O2 -DSUPPORT_LH7 -DMKSTEMP -DNEED_INCREMENTAL_INDICATOR \
  -DTMP_FILENAME_TEMPLATE=""/tmp/lhXXXXXX"" \
  -DSYSTIME_HAS_NO_TM -DEUC -DSYSV_SYSTEM_DIR -DMKTIME \
  -c -o lharc.o lharc.c

We want to make sure our version compiles with the same options as the original version, so we have to make sure to include those compiler options somewhere.

Let's save the current Makefile as a backup:

cd src
mv Makefile Makefile.old

As mentioned above, we need to define some variables in a file somewhere that our Makefiles can include. The usual place to put these defines is in a common.mk file. We can use the addvariant utility to create our initial common.mk and new Makefile, like this:

addvariant -i OS

Let's go through the common.mk file line by line to figure out what's going on, and what we need to add:

ifndef QCONFIG
QCONFIG=qconfig.mk
endif
include $(QCONFIG)

You should never change these four lines. The default qconfig.mk defines a number of variables that the Makefile system uses.

After these lines, we can define our own variables. Let's start with:

INSTALLDIR=usr/bin

This defines where to install our binary. Third-party applications should go into usr/bin instead of bin, which is the default.

Next, we put in some packager info:

define PINFO
PINFO DESCRIPTION=Archiver using lha compression.
endef

If we define PINFO information like this in our common.mk file, a lha.pinfo file will be created in each of our variant directories. We'll look at this later.

After that, we add:

NAME=lha

This tells the Makefile system what the name of our project is. Since we're building binary executables, this will be the name of our binary.

#EXTRA_INCVPATH=$(PROJECT_ROOT)/includes

EXTRA_INCVPATH defines where our header files are located. By default, all the directories from our PROJECT_ROOT down to our variant directory are added to the main include paths (i.e., where it will look for header files.) In our case, all the project headers are located in the project's root directory, so we don't need an EXTRA_INCVPATH line. This commented-out line serves as an example.

EXCLUDE_OBJS=lhdir.o makezero.o

Ordinarily, all the source code files in the PROJECT_ROOT directory are compiled and linked to the final executable. If we want to exclude certain files from being compiled and linked, we specify the object files in EXCLUDE_OBJS.

CCFLAGS=-O2 -DSUPPORT_LH7 -DMKSTEMP -DNEED_INCREMENTAL_INDICATOR
  -DTMP_FILENAME_TEMPLATE=""/tmp/lhXXXXXX"" -DSYSTIME_HAS_NO_TM
  -DEUC -DSYSV_SYSTEM_DIR -DMKTIME

The compiler flags variable is where we put the original compiler flags listed above. In QNX Neutrino 7.0 or earlier, flags defined in CCFLAGS got passed to both the C compiler, qcc, and the C++ compiler, q++. In QNX Neutrino 7.1 or later, the CCFLAGS settings are passed to the C compiler only; to pass flags to the C++ compiler, you must set them in CXXFLAGS instead.

That's all we need to add to get up and running. The last line in our common.mk file is:

include $(MKFILES_ROOT)/qtargets.mk

This does all the magic that figures out which CPU compiler to use, what binary to make, etc. You should never change this line.

Here's what our complete common.mk file looks like:

ifndef QCONFIG
QCONFIG=qconfig.mk
endif
include $(QCONFIG)

INSTALLDIR=usr/bin
define PINFO
PINFO DESCRIPTION=Archiver using lha compression.
endef
NAME=lha
#EXTRA_INCVPATH=$(PROJECT_ROOT)/includes
EXCLUDE_OBJS=lhdir.o makezero.o
CCFLAGS=-O2 -DSUPPORT_LH7 -DMKSTEMP -DNEED_INCREMENTAL_INDICATOR
  -DTMP_FILENAME_TEMPLATE=""/tmp/lhXXXXXX"" -DSYSTIME_HAS_NO_TM
  -DEUC -DSYSV_SYSTEM_DIR -DMKTIME

include $(MKFILES_ROOT)/qtargets.mk

That's it for the common.mk file. We'll see where it is included in Makefiles shortly. How about the Makefile that was just created for us? We'll very rarely have to change any of the Makefiles. Usually they just contain a LIST= line, depending on where they are in our directory tree, and some Makefile code to include the appropriate file that makes the recursion into subdirectories possible. The exception is the Makefile at the very bottom. More on this later.

We'll have to have a usage description for our application as well. In our case, we can get a usage message simply by running lha without any parameters, like this:

./lha 2> lha.use

For our final binaries, when someone types use lha (assuming lha is in their path), they'll get the proper usage message.

As described earlier in this appendix, we use a lot of subdirectories. Here are the ones we need:

Directory Level
nto OS
nto/x86_64/ CPU
nto/x86_64/o Variant

Unless we'll be releasing for QNX 4 as well as the QNX Neutrino RTOS, we'll need only the nto directory for the OS level. For the CPU level, we'll have directories for anything we want to support: ARM and/or x86_64.

The final variant directory depends on what we're building, and what endian-ness we want to compile for:

We're building just an executable binary, so we use the o variant. Each directory and subdirectory needs to have a Makefile. Again, for most of them we're simply including the recurse.mk file, which contains everything needed to recurse down our tree until we get to the o* directory, as well as setting a LIST variable, which for general use indicates where we are in our Makefile tree. For example, if the directory contains variants, LIST is set to VARIANT.

Let's use the addvariant utility to create a directory tree and appropriate Makefiles for our various CPUs and variants. The addvariant utility can do more than just add variants, but in our case, that's all we need it for. We create a variant by running:

addvariant nto 

Let's do this for each of our CPUs, like this:

addvariant nto arm o.le
addvariant nto x86_64 o

If we look at the Makefile in the lha-114i/src/nto/x86_64/o directory, we see it just contains:

include ../../../common.mk

Since this is the bottom directory, we don't need to recurse any further, but rather we want to include the common.mk file we created earlier. We also don't need a LIST variable, since we have no subdirectories at this point. We now have a complete QNX Neutrino-style Makefile tree.