What have I learned in last 7 days. Part 2: Cross compilation

20 Jul 2011

Hello again! Last week I've learned not only CMake. This article tells you how to cross-compile your C/C++/Qt application for Microsoft Windows using CMake and MinGW on your Linux machine. Let's go.

Initial conditions and problem's description

We have:

  • Linux x86 machine

  • CMake and Qt installed on our Linux system (mine are 2.8.4 and 4.7.2 respectively)

  • MinGW installed on our Linux system (mine is 3.18)

  • Qt Windows binaries compiled with MinGW. They better have the same version your Qt Linux installation has.

  • (Optional) Microsoft Windows installed on your PC or on your virtual machine. For testing purposes.

What do we want:

  • Our Qt application executable for Microsoft Windows, alive and kicking.

My method is not the only one. Other popular method offers you to compile Qt using MinGW on your Linux system instead of using Windows binaries. I've tried that, but I couldn't build Qt because of some pity bug while compiling QtCore. I would be glad if somebody helped me with that.

How CMake do its job

First, let's make clear how CMake works with Qt. The most interesting thing is what CMake does when we call find_package(Qt4). It has a special module called "FindQt4.cmake" that does all dirty work for us. This module is huge (mine is 1234 LOC long). In a couple of words FindQt4 finds QMake and query it for some useful paths and variables. It gets from QMake paths to binaries, libraries, includes, plugins - everything. And the other pals from Qt4 company - I'm talking about qt_warp_* commands - they're just wrappers for Qt tools like moc, uic and rrc.

Second, how CMake searches for other external libraries. Well, it has a few levels of search in different locations. You can read about it here. The one important thing you need to know now is that we can specify locations of search. CMake has a special variable - CMAKE_FIND_ROOT_PATH - which has one or more directories to be prepended to all other search directories. Remember that variable.

Action!

I assume you've already installed all the software from the list above. I've created a directory called crosscompile in my home and put Qt Windows binaries in crosscompile/qt. Because I use some external libraries in my projects (like pcre and openssl) I've put them here too - headers in crosscompile/include and .lib in crosscompile/lib. I've got MinGW headers and libraries in /usr/i686-pc-mingw32 and my GCC compiler executables called i686-pc-mingw32-gcc and i686-pc-mingw32-g++. That's all you need to know before we start.

First, we need to create a toolchain file for CMake. I've created it here, in crosscompile and called i686-pc-mingw32.toolchain.cmake. Here are contents of my file:

# The name of the target operating system
set(CMAKE_SYSTEM_NAME Windows)

# Which compilers to use for C and C++
set(CMAKE_C_COMPILER   i686-pc-mingw32-gcc)
set(CMAKE_CXX_COMPILER i686-pc-mingw32-g++)

# Path to libraries and headers
set(CMAKE_FIND_ROOT_PATH /usr/i686-pc-mingw32/
                         ~/crosscompile/)

# I don't know why, but CMake doesn't find headers from the var above
# Not a big deal, let's include them manually
include_directories(~/crosscompile/include/)

# Adjust the default behavior of finder
# These settings tell CMake to find programs only in systems places
# while libraries and headers will be searched only in places from
# CMAKE_FIND_ROOT_PATH variable
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

That was easy. Now you can compile simple C programs, just push -DCMAKE_TOOLCHAIN_FILE=<toolchain_file> while CMake will generate Makefile for you.

Let's get things done with Qt. As I said before CMake gets all information about your Qt installation from QMake. The problem is that this information is in QMake builtin variables and to set them we need to recompile QMake (and eventually Qt) with another QT_INSTALL_PREFIX. I made some dirty workaround for this problem:

#include <stdio.h>
#include <string.h>

#define QMAKE                 "/usr/bin/qmake"
#define QT_INSTALL_PREFIX     "~/crosscompile"
#define QT_INSTALL_DATA       "~/crosscompile/qt"
#define QT_INSTALL_HEADERS    "~/crosscompile/qt/include"
#define QT_INSTALL_LIBS       "~/crosscompile/qt/lib"
#define QT_INSTALL_PLUGINS    "~/crosscompile/qt/plugins"
#define QT_INSTALL_IMPORTS    "~/crosscompile/qt/imports"
#define QMAKE_MKSPECS         "~/crosscompile/qt/mkspecs"

int main(int argc, char const *argv[])
{

  char command[1024];

  if (argc != 3) {
    int i = 0;
    sprintf(command, QMAKE);
    for (i = 1; i < argc; ++i)
      sprintf(command, "%s %s", command, argv[i]);
    return system(command);
  }

  char * varname = argv[2];

  if (strcmp(varname, "QT_INSTALL_PREFIX") == 0)
    printf(QT_INSTALL_PREFIX); return 0;
  if (strcmp(varname, "QT_INSTALL_DATA") == 0)
    printf(QT_INSTALL_DATA); return 0;
  if (strcmp(varname, "QT_INSTALL_HEADERS") == 0)
    printf(QT_INSTALL_HEADERS); return 0;
  if (strcmp(varname, "QT_INSTALL_LIBS") == 0)
    printf(QT_INSTALL_LIBS); return 0;
  if (strcmp(varname, "QT_INSTALL_PLUGINS") == 0)
    printf(QT_INSTALL_PLUGINS); return 0;
  if (strcmp(varname, "QT_INSTALL_IMPORTS") == 0)
    printf(QT_INSTALL_IMPORTS); return 0;
  if (strcmp(varname, "QMAKE_MKSPECS") == 0)
    printf(QMAKE_MKSPECS); return 0;

  sprintf(command, "%s -query %s", QMAKE, varname);
  system(command);
  return 0;
}

It's a wrapper for QMake. It has been written in 5 minutes and I don't recommend using it instead of original QMake. Anyway, I've compiled this and put to crosscompile directory. To cheat CMake with this piece of code we need to change QT_QMAKE_EXECUTABLE variable. Just add this line to your toolchain file:

set(QT_QMAKE_EXECUTABLE "~/crosscompile/qmake")

Now you can compile your Qt application.

Wait, what's that noise? A lot of undefined references? To what? operator new[] and std::basic_string? It means you've got the same problem I had - MinGW GCC can't find standard library. To fix it you'll need libstdc++.a and libstdc++.dll.a files from your MinGW installation. I've copied them to crosscompile/lib and added these lines to my project's CMakeLists.txt:

find_library(STDC_LIB "stdc++")
...
...
target_link_libraries(... ${STDC_LIB})

Now everything should compile just fine.

Some final thoughts

Of course my solution with QMake wrapper isn't perfect. I'll say more: it's dirty and head-on. I have had not enough time to think about it when I did it, and now I don't really see any other options except the whole recompilation of Qt. I would be glad to know your ideas.