Makefile

= Create a Makefile for the Zero =

Minimal Makefile
While Makefiles can be a real pain, for small projects they usually only need a few lines. For instance when compiling a Hello World, use:

helloworld: helloworld.o    $(CC) $(CFLAGS) $^ -o $@

Or, more generally:

target: object.o    compiler flags $^ -o $@


 * target: helloworld is named a make target. It corresponds to building the helloworld executable.


 * object.o: helloworld.o is an object file. Placed after the colon, it's handled for its make target. The make program deduces to call the next line when building object file(s).


 * compiler: $(CC) is the system's default variable for the default C compiler, which is typically gcc. So, $(CC) means gcc.


 * flags: $(CFLAGS) is the system's default variable the C compiler flags. We'll assume it's just -O2.


 * $@: A place holder for the make target, helloworld.


 * $^: A place holder for the object file(s), helloworld.o.

Together without any variables or placeholders: gcc -O2 helloworld.o -o helloworld

To compile the Hello World, navigate into the directory and enter: make

helloworld should be there.

Adding a "clean" rule
Even when dealing with small projects, it can become annoying very quickly to manually remove all the objects and files created by a compilation. It can be dangerous, too; be careful not to delete your source files! If you don't use a versioning system like GIT or SVN, it can be fatal.

The easiest way to prevent removal of files that shouldn't be removed, is to create a "clean" make target: TARGET := helloworld OBJS := helloworld.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) $^ -o $@ clean: rm -f $(TARGET) $(OBJS)


 * We first define two variables, that will be used later: $(TARGET), that corresponds to the name of the executable we want to build, and $(OBJS), that corresponds to the list of the object (.o) files it depends on. It is obviously not a necesity to use variables, the Makefile could have been written without those two; it's just more convenient to use them.
 * The "clean" make target just calls "rm", which is the UNIX way of removing files, with the "-f" parameter that means "force" (it ensures it won't issue an error if the files are not present, that is, if the directory has already been cleaned), and the list of files that should be removed, contained in the variables $(TARGET) and $(OBJS). Thus, by typing "make clean", we will remove the executable and all the object files that were generated during the build. Handy!

Cross-compilation
We name cross-compilation the process of compiling a project for a different target than the host computer. For instance, compiling a project for the GCW Zero from your computer is called cross-compilation.

Writing a Makefile that handles cross-compilation as well as native compilation is surprisingly easy: TARGET := helloworld OBJS := helloworld.o CC := $(CROSS_COMPILE)gcc $(TARGET): $(OBJS) $(CC) $(CFLAGS) $^ -o $@ clean: rm -f $(TARGET) $(OBJS)


 * The $(CC) variable, which was automatically set to the system's default, is now overriden on the third line. The $(CROSS_COMPILE) variable does not appear anywhere on the Makefile; in fact, it is supposed to be passed as an environment variable to "make".
 * If the $(CROSS_COMPILE) variable is unset, the $(CC) variable will take the value "gcc"; the executable will be built for the host computer.
 * However, if it is set, the $(CC) variable will be the concatenation of the value of $(CROSS_COMPILE), and "gcc". The $(CC) variable being the one that defines the compiler to use to compile and link the program, overriding it permits to compile the project with a different compiler. For instance, if you plan to compile your project for the Zero, type "make CROSS_COMPILE=mipsel-linux-", and it will then use the compiler "mipsel-linux-gcc" for the compilation.

Cross-compile an SDL program
SDL is different than other libraries you might compile your program with, in the way that it requires a special set of C flags and linker flags, where most of the other libraries you may use won't. To get an idea of the C flags it uses, execute the command "sdl-config --cflags". To get the list of libraries that should be linked with any SDL program, execute "sdl-config --libs".

Now, the problem is that the "sdl-config" command is your PC's. So you will get the C flags, and linker flags you would use to compile natively a SDL program, which are different from those you should obtain when cross-compiling. In that case, the toolchain's "sdl-config" tool should be used. But hardcoding the first will break cross-compilation, and hardcoding the second will break native compilation and make your Makefile specific to a cross-compilation toolchain, defeating the purpose of $(CROSS_COMPILE)...

The right way of handling this, is to deduce the path to the right "sdl-config" from the compiler used: TARGET := helloworld OBJS := helloworld.o CC := $(CROSS_COMPILE)gcc SYSROOT := $(shell $(CC) --print-sysroot) CFLAGS += $(shell $(SYSROOT)/usr/bin/sdl-config --cflags) LDFLAGS += $(shell $(SYSROOT)/usr/bin/sdl-config --libs) $(TARGET): $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ clean: rm -f $(TARGET) $(OBJS)


 * The $(SYSROOT) variable gets the path of the sysroot, aka. the directory that contains the libraries and tools of the compiler. When compiling natively, it will be '/'; when compiling for the Zero, it will be '/opt/gcw0-toolchain/usr/mipsel-gcw0-linux-uclibc/sysroot'. Note the $(shell ...) keyword: it will evaluate to the text returned by the command it contains.
 * The sdl-config of the sysroot will be used to obtain the C flags and linker flags needed for the compilation. The variables $(CFLAGS) and $(LDFLAGS) are generic variables names, used in most Makefiles; you should conform to this if possible. The '+=' operator means that the value will be agregated at the end of the variable, and not overwrite it.

When using this little Makefile trickery, you ensure that your SDL program will compile natively, and cross-compile correctly.

External dependencies with pkg-config
External libraries generally provide a way to retrieve information about them, using pkg-config. Long story short, each library install a '.pc' file into /usr/lib/pkgconfig, which contain all the useful information. The 'pkg-config' tool will then read those files to print whatever information requested by the user. When dealing with an external toolchain, pkg-config can become a problem, as if not told otherwise, it will still return the information of the build system, and not the target system.

To instruct pkg-config to return information about the libraries for the target system, we use two environment variables, PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_LIBDIR: TARGET := helloworld OBJS := helloworld.o CC := $(CROSS_COMPILE)gcc DEPENDENCIES := libini SYSROOT := $(shell $(CC) --print-sysroot) PKGCONFIG := env PKG_CONFIG_SYSROOT_DIR=$(SYSROOT) PKG_CONFIG_LIBDIR=$(SYSROOT)/usr/lib/pkgconfig pkg-config CFLAGS := $(shell $(PKGCONFIG) --cflags $(DEPENDENCIES)) LDFLAGS := $(shell $(PKGCONFIG) --libs $(DEPENDENCIES)) $(TARGET): $(OBJS) $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@ clean: rm -f $(TARGET) $(OBJS)


 * PKG_CONFIG_SYSROOT_DIR will set a prefix for the include and library paths. For instance, when "pkg-config --cflags libfoo" will just return "-I/usr/include" if the environment variable is not set, it will in our case return "-I/opt/gcw0-toolchain/usr/mipsel-gcw0-linux-uclibc/sysroot/usr/include" which is the correct path.


 * PKG_CONFIG_LIBDIR will instruct pkg-config about where to find the information about the libraries; on your build system, it is /usr/lib/pkgconfig. Setting this environment variable will change that directory to "-I/opt/gcw0-toolchain/usr/mipsel-gcw0-linux-uclibc/sysroot/usr/lib/pkgconfig", which is where all the '.pc' files for our system are located.