What have I learned in last 7 days. Part 1: Cmake

19 Jul 2011

Hello there! I'm starting new series of articles about programming where I'll tell you what I have actually learned in last 7 days. I won't promise you constant updates, because, besides of all the fun, I have a job and it eats a lot of time, but I'll try. Well, let's start!

History

First, let's go a year back. I've had an experience with Python and Ruby and just started to learn C++ in the university. Among the standard C++ laboratory works we've had really cool one. It has been called "Introduction to Qt". Everybody has written "helloworlds", but I've decided to write something real. By then I've had a console program called PassMate written in Python. Its main purpose was to store my passwords. It has become my victim. Qt/C++ version has been done in a couple of weeks. It has no encryption, but only GUI. I've been excited about C++ speed and Qt power.

My primary tools these days were QtCreator and QMake simply because they were in a standard set of Qt SDK. Later I've added encryption using QCA, reinvented the UI and everything was just fine until last week.

Passmate was not the only one project I've worked on. I have another project called "Songster". Someday it'll be a music player that allows everybody to share their music. Songster is a pure C project and when I've started it I've faced a problem - what "make" system should I choose? I've had some experience with QMake, but it seemed to me like something Qt-C++ oriented. I've decided to try CMake and I liked it.

But back to the last week. Last week I decided to rewrite Passmate core in C, add an ability to store not only passwords, but files too, and make a console and GUI version. And as you may have already guessed I've used CMake to build all this stuff. That's how I've learned CMake.

But why CMake?

Honestly, I don't know. All my reasons are subjective. CMake is more difficult than QMake, but I think it's more powerful. Anyway, it was my choice. Let's see where it led me.

CMake basics

CMake settings are stored in CMakeLists.txt files. There may be several CMakeLists in your project, for example, Passmate has one main CMakeLists file in the root directory and three CMakeLists in subdirectories: for console version, for Qt version and for unit tests. That is how Passmate root directory looks like:

bin                - Here goes binaries
|
src
  |
  -- cli           - Console version sources
  |
  -- core          - Some common sources.
  |                  They are not compiled directly,
  |                  but the rest of application uses them
  |
  -- qt            - Qt version sources
  |   |
  |   -- forms     - Here goes .ui files of user interface
  |   |
  |   -- resources - Some icons collected in .rc file
  |
  -- tests         - Unit tests sources

And this is how root CMakeLists.txt looks like:

# This line always goes first
# It defines minimum version of CMake
# that is necessary to build this project
cmake_minimum_required(VERSION 2.6)

# Set our project's name
project(passmate)

# Enable unit-testing using CTest
enable_testing()

# Common compiler flags
set(CMAKE_C_FLAGS "--std=c99")

# Compiler flags for Debug build
set(CMAKE_C_FLAGS_DEBUG "-g -O0")

# Compiler flags for Release build
set(CMAKE_C_FLAGS_RELEASE "-O2")

# Find external libraries
# Variables CRYPTO_LIB and PCRE_LIB will contain
# path to libraries and necessary linker flags
find_library(CRYPTO_LIB crypto)
find_library(PCRE_LIB pcre)

# Include core files
# Because other targets use them anyway
# it's easier to create a couple of variables
# with core sources here than copy them to all subtargets
set(CORE_HEADERS ../core/cipher.h
                 ../core/storage.h
                 ../core/pwgen.h)
set(CORE_SOURCES ../core/cipher.c
                 ../core/storage.c
                 ../core/pwgen.c)

# Include core sources directory to tell compiler
# where to search for core sources
include_directories(src/core)

# Adding subdirectories with targets
# Each subdirectory should have CMakeLists.txt that will be processed
add_subdirectory(src/tests tests)
add_subdirectory(src/cli cli)
add_subdirectory(src/qt qt)

Now let's examine target's CMakeLists. First, console version:

# Add some compiler definitions
add_definitions(-DPASSMATE_CLI_VERSION=\"0.1\" -D__USE_BSD)

# This line adds executable target
# First argument - target's name
# This will be the name of our executable file in 'bin' folder
# Misc arguments - source files to compile
add_executable(passmate-cli main.c ${CORE_HEADERS} ${CORE_SOURCES})

# Link target to necessary libraries
target_link_libraries(passmate-cli ${CRYPTO_LIB} ${PCRE_LIB})

That's all. After that we'll have a cli directory in bin, which will have passmate-cli executable file.

A harder example: using CTest

That was quiet easy. Let's see CMakeLists from tests directory:

# This variable has names of the tests source files without extension
# We'll see later why
set(TESTS test_cipher test_storage test_pwgen)

foreach(test ${TESTS})

    # Compile and link our test
    # Executable will be named after test source file
    add_executable(${test} ${test}.c ${CORE_HEADERS} ${CORE_SOURCES})
    target_link_libraries(${test} ${CRYPTO_LIB} ${PCRE_LIB})

    # Add test
    add_test(${test} ${test})

endforeach(test)

Now it looks a bit tangled, but that's probably because you don't know how CTest works. It is quite simple: each unit test source file (or maybe a bunch of files – I'm using one file per module, but you can use more) compiles and when after make you type make test it runs. If your unit test returns 0 it means "everything's fine", if it crashes or returns something different from 0 it means "fail". The example above is rather simple. What if our test shouldn't return 0 on success? Well, then you can go here and read about set_tests_properties command, which allows you to manage your unit tests behavior.

Level up: building Qt applications with CMake

Here we are. If you're still reading this post I send you my greetings. Now we're gonna fight today's final boss - Qt applications. Look here:

# Find Qt
# It's necessary to use find_package instead of find_library
# because libraries are not the only things we need from Qt.
# We also need moc, uic and rcc executables.
find_package(Qt4 REQUIRED)

# Don't forget this include!
# It includes CMake file that is necessary to build Qt apps
include(${QT_USE_FILE})

# Add Qt sources
# This source files will not be processed by any Qt precompilers,
# but we'll write any processed sources in QT_SOURCES variable too
set(QT_SOURCES mainwindow.cc
                previewdialog.cc
                main.cc)

# Wrap UI files
# In the end we'll get ui_<filename>.h files
qt4_wrap_ui(QT_SOURCES forms/mainwindow.ui
                       forms/previewdialog.ui)

# Wrap resources
# In the end we'll get qrc_<filename>.cxx files
qt4_add_resources(QT_SOURCES resources/icons.qrc)

# Wrap headers
# In the end we'll get moc_<filename>.h files
qt4_wrap_cpp(QT_SOURCES mainwindow.h
                        previewdialog.h)

# Because all preprocessed files will be in our
# directory for binaries we need to include this directory so
# other sources could find them.
include_directories(${CMAKE_CURRENT_BINARY_DIR})

# Compile executable
add_executable(passmate-qt ${QT_SOURCES} ${CORE_HEADERS} ${CORE_SOURCES})

# Link executable
# Don't forget QT_LIBRARIES or you'll get undefined references to all Qt functions
target_link_libraries(passmate-qt ${QT_LIBRARIES} ${CRYPTO_LIB} ${PCRE_LIB})

It should be simpler than unit testing example. First, we find Qt libraries and binaries using find_package. Second, we preprocess everything that needs to be preprocessed, like .ui, .qrc and .h files. Finally, we compile and link executable. That's all.

The end

What can I say in the end? I've loved CMake. It's clear, lightweight, powerful and not so difficult at all. Of course I could get all these things done using QMake, but it wouldn't be so cool for me. As I said before all my reasons to use CMake were subjective and I don't want to impose you my views, but from now I will use CMake for my projects. At least until something cooler will not be invented:)