QNX Technical Articles
Make me a Millionaire, Part 1
by John FehrLet's say we have this wonderful product (we'll use the archiver, lha for this article) that we'd like to make available on all the processors QNX Neutrino supports, making our first million as a result. Unfortunately, we've been using our own custom Makefiles for gcc and x86, and we have no idea how to make binaries for other processors, or how to package up the various binaries into a package we can distribute.
Makefile mania!
Luckily, QNX Neutrino has a makefile system that, among other things, makes it very easy for us to build different processor versions with a minimum of fuss. 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.
Going straight to the source to make things happen.
First, let's grab the sourcecode for lha from:
http://www2m.biglobe.ne.jp/~dolphin/lha/prog/lha-114i.tar.gz and unarchive it with:
$ tar -zxvf lha-114i.tar.gz
This will make a directory called lha-114i. If we 'make' here, everything will compile ok, and when its done, we'll have a x86 binary called 'lha' in the src directory. Perfect!
Did someone say Options?
A typical compile 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 rename the current Makefile just in case:
$ cd src
$ mv Makefile Makefile.old
As mentioned above, we need to define some variables in a file somewhere that our Makefile's 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, with:
$ 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 will be used by the Makefile system.
After these lines, we define our own variables. Let's start with:
INSTALLDIR=usr/bin
This defines where we would like to install our binary. Third part 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. More on using the lha.pinfo file later.
After that, we have:
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 will be 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 projects root directory, so we don't need an EXTRA_INCVPATH line. I've included the commented out line as an example.
EXCLUDE_OBJS=lhdir.o makezero.o
Ordinarily, all the sourcecode 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
CCFLAGS defines what our compile flags will be. This is where we put our original compiler flags we mentioned above.
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. We should never change this line.
Here's what our complete common.mk file should look 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 we are in our Makefile 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.
Feeling used?
We'll have to have a 'use' description for our application as well. In our case, we can get a usage message simply by running lha without any parameters. Let's create our use file with:
$ ./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.
Can you say 'Restructuring'?
If you've read the Helpviewer help for QNX style Makefiles, you'll notice that we use a lot of subdirectories. Here's the basics of what we'll need:
nto/ <- OS level
nto/x86 <- CPU level
nto/x86/o <- variant level
Unless we'll be releasing for QNX4 as well as nto, we'll only need the nto directory for the OS level. For the CPU level, we'll have directories for anything we want to support: arm, sh, ppc, mips, and/or x86. The final variant directory depends on what we're building, and what endianness we want to compile for. Since x86 only has little endianness, it doesn't have an extension. If there's a choice, the variant level directory name would have a .be or .le at the end. (Ie o.le) If we were building shared libraries, we'd replace the 'o' variant with a 'so' variant. If we were building so's that aren't meant to be linked directly with apps, we'd use a 'dll' variant. If we were building static libraries, we'd use a 'a' variant. We're building just a plain old 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 contains where we are in our Makefile tree. (For example, if the directory contains variants, LIST would be set to 'VARIANT'.)
Let's use the addvariant utility to create a directory tree and appropriate Makefiles for our various CPU's 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 cpu's with:
$ addvariant nto arm o.le
$ addvariant nto sh o.le
$ addvariant nto mips o.le
$ addvariant nto mips o.be
$ addvariant nto ppc o.be
$ addvariant nto x86 o
If we look at the Makefile in lha-114i/src/nto/x86/o, 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! Try make'ing and watch our binaries being built! Sw00t! There are many more things we can do with QNX Neutrino Makefiles. I would highly recommend you browse the reference in the Helpviewer to learn more about this powerful and flexible tool.
I have a package for you.
Now that we've got binaries for all our platforms, we'll want to put them all together in a package for distribution. First, let's create a really simple qpg file that we'll use to specify which files we want to package. If we call it package.qpg, packager will automatically use it if we don't specify an alternative. It should look like:
<QPG:Generation>
<QPG:Values>
<QPG:Files>
<QPG:Add file="nto/*/*/lha" pinfo="$"/>
</QPG:Files>
</QPG:Values>
</QPG:Generation>
The only line that we need to worry about above is the 'QPG:Add file=' line. This tells the packager which files to add (nto/*/*/lha), and where it should be installing them. The pinfo="$" part tells packager to look in the lha.pinfo file that we mentioned earlier to get other information on how to package the file(s) we specified. (Such as where we want to install the binary when we do an install.)
Ordinarily, packager will create a separate qpr file for each CPU. However, if we specify the -p option, only a single qpr will be created which contains all the binaries. (And when we install, it'll install only the parts the user requests.) Let's run packager with:
$ packager -p
and use the defaults for all the questions except:
What is the full name of your product? () lha
What is your product identifier? () lha
What is the short description of this product? () lha for QNX
What is the version of this product? (1.0) 1.14i
What topic best describes this product? System/File Utilities/Compression
Enter comma-delimited keywords: () lha
Of course, if we were to build a real package for distribution, we'd answer all the questions as fully as we can. :) When its finished, we'll have our first lha-1.14i-public.qpr file!
Not again!
What if we modify our source code a little, and we want to redistribute? It's simple to 'make' again to rebuild our binaries, but do we have to answer all those packager questions again?
Nope! Luckily, packager has a -x option that'll extract most of our information from a qpr and put it into a qpg file. In our case, we would run:
$ packager -x -m lha-1.14i-public.qpr package.qpg
The -m tells packager where the default package is, and the package.qpg at the end tells packager what to call the qpg. In our case, we'll simply overwrite our original package.qpg. Now, let's modify the new package.qpg a bit: By default, the qpg file specifies not building a 'complete' package. (It normally builds qpm's and qpk's instead of qpr's.) We simply change the "no" in QPG:Repository generate="no" to "yes" inside the package.qpg file. If we scroll down to the QPG:Files section, we'll notice that the Add file lines don't look quite right. (This is because the packager has no way of knowing where the binaries inside a package originally came from, so it uses its best guess.) Let's replace those lines with our original line:
<QPG:Add file="nto/*/*/lha" pinfo="$"/>
Now whenever we want to regenerate our package file, we simply run packager with the -u option for unattended operation, and the -m followed by our previous qpr file so it knows where to get default values. Packager will automatically read the package.qpg file to figure out which files to add to our package:
$ packager -p -u -m lha-1.14i-public.qpr
Conclusion
That's all there's to it! Now go out there, and build and package your applications! I expect to see a multi-cpu lha distribution on everyone's repository! :)