I've spent more than 10 hours in total changing the build system of Trojitá from CMake to QMake upon a request from KWest. The process was very painful for me, so I think it's worthwhile to include some comments about what were the major obstacles.
The first step, "getting the beast to build", was actually pretty easy -- I
(KWest actually) simply created a list of all files in the tree, added them
into a single .pro
file, removed the #include
s for
MOC, commented out the unit tests and rebuilt the project. That was the easy
part; qmake simply built bunch of .o
files and linked them
together.
Problem with unit tests is that they all define the main()
function. Therefore, as far as I know, one has to create a separate
.pro
file for each unit test, use the template =
subdirs
and put each test into a subdirectory. That's slightly annoying
when compared to CTest (see the Trojita's git repository for how the
CMakeLists.txt
looked before we switched), but doable and
actually pretty straightforward. Now, a much bigger problem is persuading
QMake to create static libraries and using them properly and in a
cross-platform way. I care about the Unix platform, some users want to play
with Trojita on Windows, and there's that secret device KWest is building.
Trojita currently consists of several parts; we have the core IMAP stuff
(which itself consists of three or four components), the GUI layer, some
third-party modules even (like Witold Wysota's qwwsmtpclient,
the Qt's iconloader
, a unifying layer for QProcess
and QSslSocket
etc). The unit tests for the IMAP protocol
clearly should not care about icons at all and shall ignore GUI classes, too,
but they do need to link against the IMAP protocol implementation. A
reasonable way to express that in the build system is to create a static
library for the IMAP stuff and link it to the rest of the GUI when
building the application and to each of the unit tests when building tests.
That's where problems with QMake start to hurt.
Unlike CMake, under which the static libraries are extremely easy to
write, QMake's support and documentation for static libraries leaves much to
be desired. A reasonable request is, for example, expecting the build system
to isolate me from stuff like library placement -- I just want to tell it that
I need to link against bunch of .a
release and
debug
when caring about Windows builds.
Another expectation is that QMake should relink each binary if any static
library it depends on gets rebuilt. Too bad, using QMake, you have to include
the library name in three places for this to happen: at first, you have to use
LIBS += -Lpath/to/your/lib/directory
, then you got to actually
link to it by LIBS += -lnameOfTheLibrary
, and finally you have to
take care of the rebuilding by PRE_TARGETDEPS +=
full/path/and/the/libIdentifier.a
. Oh, and please do not forget about
CONFIG += ordered
, or the PRE_TARGETDEPS won't affect much stuff
anyway. Yuck!
There's the CONFIG += create_prl
and CONFIG +=
link_prl
, but they did not help me. I guess they are used for
specifying dynamic libraries on which the currently processed
static library depends. They certainly did not fix my problems when I
played with them, though.
Anyway, this is what I ended up with:
Project file for the GUI stuff,
src/Gui/Gui.pro
:
(...) trojita_libs = Imap/Model Imap/Parser Imap/Network MSA Streams iconloader qwwsmtpclient myprefix = ../ include(../linking.pri)
Each of
tests/test_*.pro
:
TARGET = test_Imap_LowLevelParser include(../tests.pri)
A helper file for unit tests,
tests/tests.pri
:
QT += core network CONFIG += qtestlib no_lflags_merge DEFINES -= QT3_SUPPORT DEPENDPATH += ../../src/ INCLUDEPATH += ../../src/ ../ TEMPLATE = app HEADERS += ../qtest_kde.h trojita_libs = Imap/Parser Imap/Model Imap/Parser Streams myprefix = ../../src/ include(../src/linking.pri) SOURCES += $$join(TARGET,,,.cpp) HEADERS += $$join(TARGET,,,.h)
And finally, the file,
src/linking.pri
:
for(what, trojita_libs) { mylib = $$replace(what,/,) unix { mypath = $$join(what,,$${myprefix},) } win32 { CONFIG( debug, debug|release ) { mypath = $$join(what,,$${myprefix},/debug) } else { mypath = $$join(what,,$${myprefix},/release) } } LIBS += $$join(mypath,,-L,) LIBS += $$join(mylib,,-l,) PRE_TARGETDEPS += $$join(mypath,,,$$join(mylib,,/lib,.a)) }
Compare the above to the elegance of CMake, depicted below out of sentiment:
set(libImap_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Imap/Parser/Parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Imap/Parser/Command.cpp ... ) ... add_library(Imap ${libImap_SRCS}) ... target_link_libraries(Imap Streams ${QT_QTGUI_LIBRARY} ${QT_QTCORE_LIBRARY}) ... target_link_libraries(trojita Imap MessageView ModelTest MSA QwwSmtpClient QtIconLoader)
That's what I call ease of use. Anyway, the QMake change just had to be done (when the customer asked if I could migrate to QMake, I said I had no preference) and it's been done now. This blog post is just a rant, and hopefully might eventually make its way to Google's results for "qmake static library".
There's always the possibility that I'm just too dumb to miss a completely
obvious way to work with static libraries in QMake. If that's indeed the case,
I sincerely apologise to QMake designers. Also, I offer one beer as a sign of
appreciation to the first person who shows me that I'm indeed missing
something from the big picture. In the meanwhile, have fun -- QMake has a lot
of nice features and I'm no longer afraid to use it, now that the static
libraries are used properly. The make test
is still missing, but
I guess I can live without that for a while.