What have I learned in last 7 days. Part 2: Cross compilation
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.