Используем Cmake для автоматической генерации makefile в проектах

  • Tutorial
  Вступление большое, так как подробно объясняет зачем нужен cmake. Можете сразу под кат, если уже знаете.

Вступление


  Компилирование проекта руками — пустая трата времени. Это фактически аксиома и об этом знают те, кто программирует. Но чтобы всё скомпилировалось автоматически необходимо задать правила, так ведь? Часто и по-старинке используют makefile для *nix или какой-нибудь nmake для windows.
  Я хоть и не первый год программирую, и руками составлял простые автосборщики проектов на основе makefile, но стоит немного подзабыть и приходится заново изучать как же составить эту хитрую схему. В основном приходится делать проекты расчитанные на какую-то одну систему, будь то linux или windows, и часто между собой не кросскомпилируемые. Для переносимости makefile используется automake и autogen, но их синтаксис ещё более запутан. Не скажу, что выбор идеальный, но для себя я решил перейти на cmake, благо он портирован под всё доступное. Мне он показался более человекопонятным. Попробую объяснить основы. Вы пишите словами правила, а из них генерируется makefile, который вы уже запускаете стандартным способом.

Ликбез

  Зачем он нужен? Чтобы при переносе на другую машину, с другими путями вы двумя командами собрали проект ничего не исправляя в файле makefile. Но есть же configure? Это альтернатива. И configure не кросплатформенный, для его генерации нужен autoconf/autogen, для которых идёт ещё свой набор правил. Только преимущества? Компиляция автосгенерированным makefile получается немного медленнее старого способа. Например, в KDE-4 является официальным инструментом выпуска.

Приступая к работе


  В текущей обучалке я расскажу о самом простом шаблоне С++ проекта, который должен компилироваться на линуксе. Ни о какой переносимости не пойдёт речи, не будут назначаться разные компиляторы и т.д. В одну статью не влезет, да и нагромождение условностей слишком запутает новичка.
  Сначала о негласно принятой структуре проектов на cmake. Обычно в корне проекта лежат папки src (для закрытых исходников), include (для публичных заголовков), lib (для подключаемых библиотек, если они не системные), пустая папка build (в ней происходит сборка, если нет, то создаём), я ещё добавляю bin (или out, для получившихся бинарников). Так же требуется наличие в корне файлов AUTHOTS, COPYING, INSTALL, NEWS, README, ChangeLog. Они могут быть пустыми, но наличие обязательно. Опциональным может быть config.h.in файл (о нём далее). Файлы конфигурации cmake лежат в каждой подключаемой папке с названием CMakeLists.txt. Это название придумано авторами cmake и не меняется.
  Документация изначально тяжела для понимания, хотя и полная. Это ещё один минус. Но по сравнению с autotools…
Список типов проектных файлов, которые может генерировать cmake:
Скрытый текст
Borland Makefiles
MSYS Makefiles
MinGW Makefiles
NMake Makefiles
NMake Makefiles JOM
Ninja
Unix Makefiles
Visual Studio 10
Visual Studio 10 IA64
Visual Studio 10 Win64
Visual Studio 11
Visual Studio 11 ARM
Visual Studio 11 Win64
Visual Studio 6
Visual Studio 7
Visual Studio 7 .NET 2003
Visual Studio 8 2005
Visual Studio 8 2005 Win64
Visual Studio 9 2008
Visual Studio 9 2008 IA64
Visual Studio 9 2008 Win64
Watcom WMake
Xcode
CodeBlocks — MinGW Makefiles
CodeBlocks — NMake Makefiles
CodeBlocks — Ninja
CodeBlocks — Unix Makefiles
Eclipse CDT4 — MinGW Makefiles
Eclipse CDT4 — NMake Makefiles
Eclipse CDT4 — Ninja
Eclipse CDT4 — Unix Makefiles


  Пусть у нас есть некий source.cpp из которого получаем софт — положим его в папку src. Но у нас же проект, поэтому есть ещё несколько файлов core.cpp, core.hpp, common.hpp, types.hpp, которые тоже положим в src и нужна какая-нибудь библиотека, например pthread. Разобрались с исходниками, приступаем к описанию проекта и подготовке его к автокомпилированию.

  Начинается всё с создания в корне проекта файла CMakeLists.txt. Правила cmake похожи на скриптовый язык, среднее между javascript и php. Только проще гораздо. Есть условия, функции, переменные, константы, подключаемые модули.

  Файл CMakeLists.txt я разобью на несколько частей, чтобы объяснять их. Часть 1:
# комментарии начинаются с решётки
cmake_minimum_required (VERSION 2.6) 

cmake_policy(SET CMP0011 NEW)
cmake_policy(SET CMP0003 OLD)

OPTION(WITH_DEBUG_MODE "Build with debug mode" ON)

if ( NOT UNIX )
    message (FATAL_ERROR "Not Unix!")
endif ()

  Тут cmake_minimum_required функция проверки версии.
Операторы записываются как if () открывающий и endif() закрывающий. Аналогично foreach() и endforeach().
Функция message выводит наше сообщение. Я использовал флаг FATAL_ERROR для обозначения типа сообщения.
Заметьте так же, что не ставится точка с запятой (;) в конце команд. Тут одна строка — одна команда. Скобки отодвинуты от операторов просто для удобства чтения.
Каждая команда обычно имеет несколько вариантов задания параметров, поэтому без заглядывания в мануал не обходится никак.
Быстрое ознакомление и простой пример есть в документации, но на мой взгляд слишком простые.

Часть 2:
message ("Starting cmake")

# я вынес настройки путей, флаги компиляции в отдельный фаил, чтобы не громоздить здесь лишнего
include (myproj.cmake)

# создаём новый проект
set (PROJECT myproj)

# в текущем проекте ничего не нужно дополнительно компилировать
set (LIBRARIES)
# следующий код нужен для компиляции и подключения сторонних библиотек 
    foreach (LIBRARY ${LIBRARIES})
        find_library("${LIBRARY}_FOUND" ${LIBRARY})
        message(STATUS "Check the ${LIBRARY} is installed: " ${${LIBRARY}_FOUND})
        if ( "${${LIBRARY}_FOUND}" STREQUAL "${LIBRARY}_FOUND-NOTFOUND" )
            message(STATUS "Adding library sources")
            add_subdirectory (../${LIBRARY} lib/${LIBRARY})
        endif ()
    endforeach ()

# никаких дополнительных целей нет
set (TARGETS "")

set (HEADERS "")

message ( STATUS "SOURCES: ${SOURCES}")

add_subdirectory (src)

  Функция set() создаёт или перезаписывает переменные. Если нет значения, то переменная будет пустой. Заданные здесь переменные названы по смыслу, который они несут в генерации.
include() подключает внешний файл с куском конфигурации в текущее место. Тут без объяснений.
И add_subdirectory (src) указывает где продолжить работу по сборке makefile проекта.

  Если здесь задавались только общие правила, то внутри директории src в CMakeLists.txt будут заданы опции объединения конкретных исходных файлов.

  Не упомянул ещё о cmake_policy(). Решил не озадачивать сейчас вниканием в это. Пускай тут повесит © Просто облегчает сборку.
Про foreach() цикл и библитеки будет рассказано дальше. Пропустим пока.

  Что же такого было вынесено в отдельный cmake файл? Давайте рассмотрим:

set ("${PROJECT}_BINARY_DIR"  bin)
set ("${PROJECT}_SOURCE_DIR" src:include)
set ("${PROJECT}_LIB_DIR" lib)

set (CMAKE_INCLUDE_PATH ${${PROJECT}_SOURCE_DIR})
set (CMAKE_LIBRARY_PATH ${${PROJECT}_LIB_DIR})
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/${${PROJECT}_BINARY_DIR})
set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_BUILD_TYPE Debug)

set (ERR_NO_UNIX "Cannot build on non Unix systems")

if ( WITH_DEBUG_MODE )
     ADD_DEFINITIONS( -DMY_DEBUG_MODE=1)
endif()

if ( CMAKE_COMPILER_IS_GNUCXX )
    set(MY_CXX_FLAGS  "-Wall -std=c++0x -fmessage-length=0 -v -L/usr/local/lib -L/usr/lib")
    set(CMAKE_CXX_FLAGS "-O0 ${MY_CXX_FLAGS}")
    # я отключил настройку разных флагов для релиза и отладки. Пока что не нужно.
    #set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fno-reorder-blocks -fno-schedule-insns -fno-inline")
    #set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
else ()
    message (FATAL_ERROR ${ERR_NO_UNIX})
endif ()

  Видим, что в set () использована странная (или уже привычная) конструкция в виде ${название} — она подставляет раннее определённую переменную с помощью того же set () или самим cmake (все определяемые cmake переменные есть в документации). Например ${PROJECT} вставит значение myproj из ранее определённой переменной PROJECT.
Запись src:include — просто строка, которая в линуксе означает перечисление путей (они там разделяются двоеточием, а не точкой с запятой).
  Первые три строки это мною заданные переменные. А вот дальше задаются необходимые переменные, требуемые cmake. Их можно и не задавать явно, они уже определены с запуском, но будут указывать не туда, куда нужно с данной структурой папок.

  Порою необходимо название переменной составлять исходя из других переменных, и такое возможно: ${${PROJECT}_SOURCE_DIR}. Сначала разыменуется PROJECT и получится ${myproj_SOURCE_DIR}, которая уже определена вначале. В итоге будет её значение. Всё это необходимые сложности, дабы если поменяется название проекта с myproj на superpuper не пришлось лазить по всему коду меняя названия переменных и прочего.

  В блоке if ( CMAKE_COMPILER_IS_GNUCXX ) определили какие флаги используются для компиляции. Пока что не кроссплатформенной, но используя ветвления и различные созданные при старте переменные-флаги можно нагородить разные зависимые билды для разных платформ и компиляторов. Весь список переменных находится в разделе Variables и раздел этот довольно большой. В прочем, этот блок можно убрать совсем из конфига или подготовить с помощью обширного функционала cmake. Но тут заняло бы слишком много места.

  if ( WITH_DEBUG_MODE ) — встречается особый вид переменных, которые начинаются на WITH_. Создаются они с помощью функции option() — функции, которая определяет пользовательские опции (она была в коде в самом начале). Эти переменные принимают только два значения ON или OFF и могут использоваться в качестве булевых значений, к примеру. И ADD_DEFINITION( -DMY_DEBUG_MODE=1) добавит компилятору опцию -DMY_DEBUG_MODE, если включён дебаг режим. Для С++ компилятора (в данном случае) это будет означать добавление дефайна.

Работа с исходным кодом


Вот и добрались до папки src. В ней тоже необходим CMakeLists.txt, рассмотрим его детальнее.
set ("${PROJECT}_VERSION_MAJ" 0)
set ("${PROJECT}_VERSION_MIN" 1)
set ("${PROJECT}_VERSION_A" 1)
set ("${PROJECT}_VERSION_B" 1)
set ("${PROJECT}_VERSION" ${${PROJECT}_VERSION_MAJ}0${${PROJECT}_VERSION_MIN}0${${PROJECT}_VERSION_A}0${${PROJECT}_VERSION_B})

message(STATUS ${${PROJECT}_VERSION})

# основной файл программы
set (MAIN_SOURCES
    source.cpp
    )

# непубличные пары исходников
set (PRIVATE_CLASSES
        core
    )

# файлы только заголовочные, у которых нет пары-исходника
SET (HEADERS_ONLY
        types
        common
    )

# публичные исходники
set (PUBLIC_CLASSES)

# используемые в программе библиотеки
set (ADDITIONAL_LIBRARIES
    stdc++
    pthread
    )

set (PUBLIC_HEADERS)
set (SOURCES)

foreach (class ${PRIVATE_CLASSES})
    LIST (APPEND SOURCES ${class}.cpp)
    LIST (APPEND HEADERS ${class}.hpp)
endforeach ()

foreach (class ${HEADERS_ONLY})
    LIST (APPEND HEADERS ${class}.hpp)
endforeach ()
    
foreach (class ${PUBLIC_CLASSES})
    LIST (APPEND SOURCES ${class}.cpp)
    LIST (APPEND HEADERS ../include/${PROJECT}/${class}.hpp)
    LIST (APPEND PUBLIC_HEADERS ../include/${PROJECT}/${class}.hpp)
endforeach ()

add_executable (${PROJECT} ${MAIN_SOURCES} ${SOURCES})

target_link_libraries (${PROJECT} ${ADDITIONAL_LIBRARIES})

set_target_properties(${PROJECT} PROPERTIES VERSION "${${PROJECT}_VERSION}" SOVERSION "0")

INSTALL (
    TARGETS
    ${PROJECT}
    DESTINATION
    lib${LIB_SUFFIX}
)

INSTALL (
    FILES
    ${PUBLIC_HEADERS}
    DESTINATION
    include/${PROJECT}
)

  Здесь записаны конкретные файлы и их сборка. Вначале определили версию программы. Дальше определяем группы (или как их тут называют, списки) исходников, все названия записываются через пробел. MAIN_SOURCES — один основной исходник, PRIVATE_CLASSES — список пар исходников (исходник.cpp-заголовок.hpp с общим названием), PUBLIC_CLASSES — для данного проекта пустой, HEADERS_ONLY — список лишь заголовочных файлов, ADDITIONAL_LIBRARIES — подключаемые в проект С++ библиотеки.

  Следующими циклами объединяются списки заголовков и исходников в один подключаемый список (замечу, что предыдущее разделение на публичный и скрытый было чисто условным). И, наконец, правило обозначающее «собрать проект в запускаемый файл» add_executable(). После сборки бинарника к нему нужно добавить «прилинковать» подключаемые библиотеки с помощью target_link_libraries() и всё. Правила проекта определены.
Для удобства можно добавить правила установки-инсталяции с помощью install().

  Переходим в директорию build, что в корне проекта, и запускаем
cmake ..
из командной строки. Если всё прошло удачно (а должно пройти удачно), получаем
— Configuring done
— Generating done
— Build files have been written to: /home/username/tmp/fooproj/build

и находящийся рядышком makefile
После чего уже привычным способом
make

Вместо заключения


Я нашёл данный способ более простым и удобным, нежели конфигурирование configure. Если вдруг захочется по спорить на этот счёт, то я воздержусь от комментирования. Потому что зачем?
Если вы что-то поменяли и cmake выдаёт ошибки, то сперва удалите весь кеш файлов из директории build (грубо говоря всё) или как минимум CMakeCache.txt из той же директории билда.

Что было не рассмотрено в обучалке:
1) Подключение библиотек Qt или Boost
2) Поиск установленных библиотек
3) Мультиплатформенность
4) Опции различных компиляторов

Документация:

1) Основная странцица документации
2) Документация по версии 2.8.9
3) Исходник этого безобразия прилагается.
Share post

Comments 45

    –4
    Вопрос только один — почему не GYP?
      +9
      «Так повелось» © Извините, что грубо, но данный пост про cmake.
        –5
        Ну знаете, вы меня извините пожалуйста, но это не ответ. Я хотел узнать почему именно cmake, а не какая-нибудь другая более новая система?
          +5
          Я имею право не отвечать на вопрос?
            0
            И все же, было бы интересно: Почему именно он?
            Сейчас собираюсь применять, а для этого мне надо подружить CMake с Doxygen, Boost.Test, Precompiled headers компиляцией и Запуском некоторых кастомных python-скриптов тестирующих собранное. Было бы круто если вы все же ответили )
              0
              Просматривая большие проекты в поисках замены autotools, увидел cmake, потом scons — они известны, они используются; сравнив выбрал cmake. Gyp тоже видел, не понравился. Я с этим ничего не могу поделать.
                0
                >>Gyp тоже видел, не понравился.
                Не понравился чем конкретно? Мы же не девушки выбирающие сумку по цвету там туфель или еще чего-нить.

                Пока вижу плюсы у Gyp, хоть и узнал о нем не больше часу назад.
                + На Json, этот формат сейчас чуть ли не все языки программирования знают что дает в случае чего писать свои кастомные скрипты для решения каких-либо задач;
                + Более читабелен синтаксис, а я пытался разобраться с CMake-синтаксисом, но поняв что достаточно сложен и поэтому отложил «на потом»;
                  –3
                  Пожалуйста, прекратите переубеждать меня в том, что нравится вам. Да и почему вы думаете, что программист не может выбирать себе autotool исходя из того, нравится синтаксис или нет? :)
                    0
                    Никто не убеждает. Вас просят перечислить список недостатков которые вы заметили в Gyp в результате которых вы приняли решение что не нравится? Это только из-за синтаксиса Gyp, правильно понял?
                      0
                      Я не могу найти — может ли gyp искать сторонние библиотеки, судя по всему — нет. В таком случае, имхо, это его критический недостаток для многих проектов.
                        0
                        Я не собираю стопитсот зависимых проектов одновременно, если вы намекаете на какое-то особое удобство gyp в этом деле. Только из-за json синтаксиса. Может быть есть особенности функционала свои, но я их не изучал в виду того, что откинул его сразу. Вот SCons да, тот изучал и мне он даже показался проще cmake. Но изучать из-за scons питон времени не было. Так повелось ©
                  +1
                  Про precompiled headers + cmake можно посмотреть тут: www.cmake.org/Bug/view.php?id=1260.
          0
          Может быть, вы вкратце поясните, чем gyp лучше cmake? Не могу найти сходу ни одного преимущества за исключением спорной новизны.
            0
            Как указано в следующем комментарии — здесь все подробно описано code.google.com/p/gyp/wiki/GypVsCMake. А если брать в расчет удобства для меня как для разработчика:

            • Только одна зависимость — python, который, к слову, по-умолчанию доступен в большинстве дистрибутивов linux.
            • Можно генерировать .ninja файлы ( martine.github.com/ninja/ ), а, с их помощью, очень быстро компилировать проекты.
            • Легкий и понятный псевдо-JSON синтаксис
              0
              Вы предлагаете, например, для сборки проекта на C тащить ещё и Python? Но зачем? Весь мир сошёл с ума с интерпретируемыми языками.

              CMake прекрасен тем, что действительно работает на чём угодно и при этом не тащит за собой никаких зависимостей.
                –1
                Никаких зависимостей, кроме cmake.
                  +1
                  А gyp уже не зависимость? Или его каждый таскает за собой прямо в проекте? Кстати например в двух дистрбутивах которыми я пользуюсь gyp нет. А CMake есть везде.
                0
                CMake тоже прекрасно работает с ninja.
                  +1
                  cmake тоже поддерживает ninja.
                  И компиляция всего MySQL этим ninja нисколько не быстрее, чем GNU make, даже медленнее.
                  –4
                  И еще одно — GYP может генерировать не только Makefile, но и xcodeproject и vcproject тоже.
                    0
                    CMake тоже это умеет. Например, «cmake -G Xcode» для xcodeproject.
                      0
                      cmake тоже умеет генерировать не только Makefile, иначе в нем не было бы смысла. список генераторов: www.cmake.org/cmake/help/v2.8.9/cmake.html#section_Generators
                        –1
                        Согласен, не увидел этого.

                        А вот как у cmake обстоит дело с зависимостями? Допустим у меня есть библиотека с cmake файлом (и 500+ C файлами), которую я хочу использовать в своем cmake проекте (не добавляя эти 500 файлов руками). Может ли cmake подключить ее?
                          0
                          Вы про TARGET_LINK_LIBRARIES в cmake?
                            0
                            Да, может. Что то в духе:

                            file (GLOB_RECURCE my_srcs *.c)
                            add_library(mylibrary SHARED my_srcs)
                            
                              –1
                              Это конечно хорошо, но допустим там есть разные файлы для разных платформ (проще говоря сложная логика). Вы же не собираетесь полностью скопировать все зависимости и логику из cmakefile библиотеки?
                                +1
                                Подождите, вы что сейчас хотите доказать? Ну хорош ваш GYP, ну и что? Статья про Cmake, и многим интересна. Напишите про GYP, так же почитаем с удовольствием.
                                Судя по вашей логике, статей, например, про ассемблер, уже лет двадцать-тридцать как не должно быть, ведь придумали же… где… и т.д.
                                  –1
                                  Согласен, статья не помешала бы. Я просто хочу обратить внимание разработчиков на другие системы (cmake, как и любую другую, нельзя назвать совершенной).
                                  +2
                                  Ну я надеюсь, весь платформеный код лежит в отдельных папках (ну или по крайней мере есть способ отобрать файлы с помощью тех же глобов). Никто не запрещает указать несколько списков файлов в качестве параметра add_library или, наоборот, собрать сначала нужные исходники в один список, а потом отдать их в add_library. Получится что то в духе этого:
                                  file (GLOB_RECURCE my_common_srcs *.c)
                                  if (WIN32)
                                    file(GLOB_RECURCE my_platform_srcs win32/*.c)
                                  else()
                                  ...
                                  endif()
                                  add_library(mylibrary SHARED my_common_srcs my_platform_srcs)
                                  


                                  В целом CMake принято ругать скорее за сложный синтаксис, чем за недостаток фич.
                                    0
                                    Еще есть один нюанс cmake, то что после добавление нового файла в каталог с исходниками нужно ЗАНОВО перегненирировать все файлы для всех build modes (при использовании масок типа *.cpp). Хотя конечно это отражено в описании cmake, т.к. cmake это не система сборки, это всего лишь генератор файлов для сборки и в этом его и плюс и минус. Минус я считаю достаточно большой, т.к. подобные системы не могут живо реагировать на изменение рабочего окружения, и чем больше и сложнее проект, тем больше вероятность что-то упустить из виду.
                                      +1
                                      Лучше добавлять файл руками, тогда все перегенерится само при сборке. К тому же, собирать все файлы далеко не всегда надо.
                        0
                        Ага, тоже удивился. Есть еще WAF, SCONS.

                        code.google.com/p/gyp/wiki/GypVsCMake
                          0
                          Спасибо, я не знал что существует столь интересный проект )
                          +4
                          Но Qt пользуются cmake и никто не жалуется.

                          Гм, что? Qt сейчас собирается с помощью configure + qmake. Приложения, использующие Qt, обычно собираются с помощью qmake. В планах переход на Qt Build Suite. Причем тут cmake?..

                          К слову, cmake для MSVS/XCode/CodeBlocks/etc генерирует не Makefile'ы, а проектные файлы для соответствующих IDE.
                            0
                            Пасибо, щас исправлю эту часть. Не в Qt, а стандартное для KDE4.
                            0
                            Не будет ли уместно запостить в *nix? Понятно что технология кросплатформенная, но все же
                              +1
                              Сделал :)
                              +1
                              На хабре как раз нехватало туториала по CMake, даже сам думал написать :)
                                0
                                Ога, официальная дока заставляет писать с бубном, но на просторах инэта есть и на русском, которые хоть как-то поясняют как эта магия работает
                                  0
                                  Ну оно не сильно сложно, но гуглить иногда приходится, да.
                                    0
                                    Ага, там далеко не всё очевидно и иногда попадаются забавные баги типа того, что на Mac OS X оно всегда декларирует систему как 32-битную и приходится писать вот такое, например:

                                    IF (CMAKE_SYSTEM_PROCESSOR MATCHES "^(i.86|x86|x86_64)$")
                                    	INCLUDE(CheckTypeSize)
                                    	CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P BUILTIN_TYPES_ONLY)
                                    	IF (${SIZEOF_VOID_P} EQUAL 8)
                                    		SET(CMAKE_SYSTEM_PROCESSOR x86_64)
                                    	ELSE()
                                    		SET(CMAKE_SYSTEM_PROCESSOR i386)
                                    	ENDIF()
                                    ENDIF()
                                    
                                      0
                                      У них есть багтрекер :) Да и разработчики вменяемые люди, так что стоит туда отчет отправить.
                                        0
                                        У них в багтрекере оно уже очень давно есть, однако воз и ныне там.

                                        И это, в общем, практически везде так. Я по этой причине давно забил общаться с opensource-проектами. Последний раз постил года полтора назад баг в SWT, связанный с отображением фона контрола на Mac OS X, так его до сих пор не пофиксили, хотя уже несколько релизов прошло. Про своё общение с командой BSD я вообще предпочитаю не вспоминать, ибо это тоска.
                                          0
                                          Частично соглашусь, не все мои ошибки, про которые я писал отчеты в опенсорсных проектах исправлялись, но я бы не сказал толку их писать нет.
                                          С CMake у меня был такой опыт. Мне нужен был определенный функционал, я написал модуль и решил передать его в апстрим. Мне рассказали, что надо дооформить, кое-где подправили, в общем всячески помогали.
                                          Так что если можете не только отчет написать, но и исправить ошибку — думаю, ваш патч примут.
                                          Это же оперсорс — кто угодно может принять участие в разработке и вы тоже :)

                              Only users with full accounts can post comments. Log in, please.