Инструментальные средства генерации make-файлов и проектов

В статье рассматриваются инструменты для генерации make-файлов и проектов таких IDE, как Visual Studio. Статья будет интересна тем, кто лишь недавно опрометчиво выбрал тяжкий труд программиста.


Утилита make и Makefile

Преимущества разбиения исходных текстов разрабатываемой программы на множество отдельных файлов очевидны. Однако, после такого разбиения усложняется процесс компиляции программы. Например, пусть мы разрабатываем программу, исходые коды которой разбиты на два файла: main.c и fun.c. Тогда скомпилировать их можно было бы такой командой (предположим, что работа выполняется в GNU/Linux):
gcc main.c fun.c -o superproga

Недостатки такого подхода следующие:

  1. Если мы изменили только один файл, например, main.c, то fun.c все равно будет тоже перекомпилироваться, хотя соответсвующий объектный файл был создан во время предыдущих компиляций. Поэтому было бы неплохо выполнять компиляцию только тех файлов, которые изменились, т.е. тех, у которых время модификации "позже", чем соответствующих объектных.
  2. Проект может содержать десятки и сотни файлов, и перечислять их всех довольно лениво.

Для решения указанных проблем, а также автоматизации процесса сборки больших программных проектов когда-то в древности была разработана утилита make. Эта утилита читает специальный файл (обычно он называется Makefile) и выполняет указанные команды, если в этом есть необходимость. Основу Makefile составляют строки следующего вида:
цель: зависимости
                [tab] команды
Важно отметить, что строки с командами начинаются именно с табуляции, а не с нескольких пробелов1. Для нашего примера Makefile может быть следующим:
prog: main.o fun.o
        gcc main.o fun.o -o prog

fun.o: fun.c fun.h
        gcc -c fun.c

main.o: main.c
        gcc -c main.c
make запускается следующим образом:
make prog
Если цель в командной строке не указывать, то make будет выполнять цель, которая встретиться в файле первой.

Теперь если изменить лишь какой-то один файл и выполнить команду make, можно увидеть, что компилируется только этот измененный файл 2.

Вариант make из проекта GNU - gmake - является довольно продвинутым инструментом, существенно упрощающим тяжелую жизнь программистов. Наш Makefile можно немного изменить, сделав более универсальным:
SRCS = main.c fun.c
OBJS = ${SRCS:%.c=%.o}

.SUFFIXES: .c .o

.c.o:
        gcc -c $< -o $@

prog: ${OBJS}
        gcc ${OBJS} -o prog

fun.o: fun.h

Следует подчеркнуть, что хотя make разрабатывалась для автоматизации процесса сборки программ, она может с успехом применяться и для других целей. Ведь никто не запрещает в качестве команд указывать что-угодно. Например, следующее правило позволяет перегенерировать документацию по проекту, если она устарела:
doc: fun.h
    doxygen

Современные интегрированные среды разработки (IDE 3) также позволяют создавать проекты и выполнять компиляцию только изменившихся файлов. Как правило, эти средства имеют якобы более удобный интерфейс, позволяя создавать проект с помощью команд меню или мышкой, но гораздо меньшие возможности, так как умеют делать только то, что запрограммировали авторы IDE.

Подробную документацию об утилите make можно найти на соответствующих man- и info-страницах, а также на просторах Internet. Очень хорошее введение лежит здесь.

Генераторы Makefile-ов

В добрые старые времена4 функциональности make было вполне достаточно. Но с тех пор все ужасно улучшилось. Во-первых, программы обладают гораздо большей функциональностью и, как результат, требуют для своей работы гораздо больше библиотек. Было бы неплохо определять наличие отсутствия :) библиотеки еще на этапе компиляции. Во-вторых, операционные системы стали более разнородными. И если в нормальных *nix и Linux всего несколько каталогов, в которых находятся библиотеки (/usr/lib, /usr/local/lib) и другие файлы, необходимые для сборки, то во всяких Windows, программы устанавливаются куда попало. И определить, куда на машине известного Василия Пупкина установлена, например, библиотека expat довольно тяжело. Все это приводит к тому, что написание универсального, а тем более переносимого, Makefile-а становится довольно сложной задачей.

В соответствии с принципом "Пишите программы, которые пишут программы" 5 ленивые программисты написали несколько генераторов Makefile-файлов. Одним из первых таких генераторов был набор утилит autoconf и automake. Вот в этой статье рассказывается об использовании данных утилит.

Позже появились такие утилиты, как qmake, bakefile, CMake и ряд других. qmake является ачстью библиотеки Qt и заточена на использование именно этой библиотеки. bakefile в настоящее время имеет версию 0.2. Насколько мне известно, с помощью этой утилиты собирается библиотека wxWidgets . CMake является довольно продвинутым и развитым инструментом. С его помощью собираются несколько крупных проектов, например, KDE. Следующий текст я скопипестил с официального сайта этой утилиты:
A Summary Of Features
CMake is an extensible, open-source system that has several powerful features. These include:
  • Supports complex, large build environments. CMake has been proven in several large projects.
  • Generates native build files (e.g., makefiles on Unix; workspaces/projects on MS Visual C++). Therefore standard tools can be used on any platform/compiler configuration.
  • Has powerful commands include the ability to locate include files, libraries, executables; include external CMake files that encapsulate standard functionality; interfaces to testing systems; supports recursive directory traversal with variable inheritance; can run external programs; supports conditional builds; supports regular expression expansion; and so on.
  • Supports in-place and out-of-place builds. Multiple compilation trees are possible from a single source tree.
  • Can be easily extended to add new features.
  • CMake is open source.
  • CMake operates with a cache designed to be interfaced with a graphical editor. The cache provides optional interaction to conditionally control the build process.

По этой причине6 рассмотрим эту утилиту более подробно.

Примеры использования CMake

CMake читает специальный файл (обычно он называется CMakeLists.txt) и выполняет указанные команды, создавая Makefile. Команды во входном файле имеют следующий вид:
команда(аргументы)
CMake понимает большое количество команды, аргументы которых - строки с разделенными пробелами словами.

Для нашего примера CMakeLists.txt будет иметь следующий вид:
PROJECT (prog)
SET (PROJECT_SOURCE_LIST main.c fun.c)
ADD_EXECUTABLE(prog ${PROJECT_SOURCE_LIST})
С помощью первой строки мы задаем имя для workspace, которое будет создано при генерации проекта для Visual C++. Во второй строке мы создаем переменную ${PROJECT_SOURCE_LIST}, которая содержит названия файлов с исходными текстами. В третьей строке мы сообщаем из каких файлов создавать исполняемый файл. Имена файлов извлекаются из созданной переменной.

Для генерации Makefile необходимо запустить Cmake. Версия утилиты для Windows имеет стандартный графический интерфейс:
Windows-версия CMake
Исполняемый файл называется CMakeSetup.exe. Версия для *nix может иметь curses-интерфейс.
Unix-версия CMake
В этом случае исполняемый файл называется ccmake.

В *nix при запуске утилиты в командной строке следует указать каталог, в котром содержится подготовленный командный файл. В случае Windows этот каталог можно указать из окна утилиты. После запуска утилиты надо выполнить дополнительное конфигурирование, нажимая клавишу 'c' (в лучае *nix-версии) или кнопку 'Configure' (в Windows). Возможно надо будет выполнить несколько нажатий. При этом при работе в Windows можно указать тип создаваемого проекта: проект для Visual Studio разных версий или варианты make-файла. После того, как конфигурирование выполнено можно сгенерировать make-файл или проект для IDE, нажав, ставшие доступными, клавишу 'g' или кнопку 'Ok'.

Существует также неинтерактивная утилита cmake, которая молча выполняет свою работу. Ее удобно использовать, когда файл CMakeLists.txt уже должным образом настроен и необходимо лишь время от времени добавлять в проект файлы. Также эта утилита может использоваться в скриптах. Например, можно сочинить скрипт, который после обновления файлов из репозитария CVS или SVN проверяет, менялись ли файлы CMakeLists.txt, и при необходимости запускает указанную утилиту.

CMake умеет создавать Makefile-ы для проектов, исходные тексты которых располагаются в нескольких каталогах. Для этого следует создать CMakeLists.txt в каждом каталоге с исходниками, а в файле, лежащем в корневом каталоге проекта, перечислить подкаталоги. Вот фрагмент CMakeLists.txt одного из моих проектов с исходниками, которые для удобства размещены в нескольких каталогах:
PROJECT (dmt)
SUBDIRS(dmt sysed tests)
CMake содержит пример проекта "Hello, World!", который также располагается в нескольких каталогах.

Продвинутые возможности CMake

Cmake, как и другие генераторы, позволяет при формировании Makefile-ов выполнять поиск необходимых для проекта библиотек. Рассмотрим эти возможности на примере. Предположим, нашей программе крайне необходима библиотека expat. CMake содержит следующую команду для поиска библиотеки:
FIND_LIBRARY(VAR libname [path1 path2...])
Эта команда ищет библиотеку libname в каталогах pathi. Если библиотека будет найдена, то в VAR будет записан результат, в противном случае VAR примет значение NOTFOUND, которое CMake рассматривает как "ложь". В справке по Cmake приводится также расширенная форма этой команды.

Также нам необходимо найти каталог, в котором располагается файл expat.h. Позже этот каталог будет добавлен к другим include-каталогам проекта. Поиск каталога выполняет команда:
FIND_PATH(VAR filename [path1 path2...])
Действие этой команды очевидно.

Поместив эти команды в CMakeLists.txt, мы решим нашу задачу. Однако, можно поступить по-другому. Дело в том, что CMake позволяет создавать дополнительные модули, которые могут выполнять часто встречающиеся задачи. В подкаталоге Modules (например, /usr/local/share/CMake/Modules) находится большое количество таких дополнительных модулей. Написав такой модуль 7, мы

  1. Упростим себе жизнь, ведь в будущем снова сможем его использовать.
  2. Сделав модуль общедоступным, немного осчастливим человечество :)
Разработанный модуль (FindExpat.cmake) может быть следующим:
# Find the native EXPAT include and library
FIND_PATH (EXPAT_INCLUDE_DIR expat.h
    /usr/include
    /usr/local/include
    "C:/Program Files/Expat/Include"
)
IF (WIN32)
        SET(LIBEXPAT_NAME libexpat)
ELSE (WIN32)
        SET(LIBEXPAT_NAME expat)
ENDIF (WIN32)

FIND_LIBRARY (EXPAT_LIBRARY ${LIBEXPAT_NAME}
    /usr/lib
    /usr/local/lib
    "c:/Program Files/Expat/Libs"
)
SET (EXPAT_FOUND "NOTFOUND")
IF (EXPAT_LIBRARY AND EXPAT_INCLUDE_DIR)
    SET (EXPAT_FOUND "FOUND")
ENDIF (EXPAT_LIBRARY AND EXPAT_INCLUDE_DIR)

MARK_AS_ADVANCED (EXPAT_INCLUDE_DIR EXPAT_LIBRARY)

#Log
FILE (WRITE "${PROJECT_BINARY_DIR}/findexpat.log" "Expat found: ${EXPAT_FOUND}\n")
FILE (APPEND "${PROJECT_BINARY_DIR}/findexpat.log" "Expat library name: ${LIBEXPAT_NAME}\n")
FILE (APPEND "${PROJECT_BINARY_DIR}/findexpat.log" "Expat include: ${EXPAT_INCLUDE_DIR}\n")
FILE (APPEND "${PROJECT_BINARY_DIR}/findexpat.log" "Expat lib: ${EXPAT_LIBRARY}\n")
Думаю, код модуля достаточно понятен. Название библиотеки в *nix и Windows немного отличаются, поэтому приходится использовать конструкцию IF. Последние несколько строк записывают в корневой каталог проект результат рабты модуля.

Подключается модуль в файл CMakeLists.txt командой
INCLUDE(FindExpat)
Эта команда считывает указанный модуль и выполняет записанные в нем команды.

В качестве еще одного примера приведу фрагмент CMakeLists.txt, который применяется в одном из моих проектов:
#wxWidgets build related stuff
SET(WXW_USE_DEBUG OFF)
SET(WXW_USE_UNICODE ON)

IF (WIN32)
	SET(WXW_USE_SHARED OFF)
ELSE (WIN32)
	SET(WXW_USE_SHARED ON)
ENDIF (WIN32)

SET(WXW_USE_UNIV OFF)
SET(WXW_USE_MONO OFF)
SET(WXW_FILE_VERSION "26")
SET(WXW_VERSION "2.6")

#CMake Options
SET(CMAKE_VERBOSE_MAKEFILE TRUE)

#Additional libraries
INCLUDE (FindwxW)
INCLUDE (FindExpat)

IF (NOT EXPAT_FOUND)
    MESSAGE (FATAL_ERROR
        "Can't find expat library"
    )
ENDIF (NOT EXPAT_FOUND)

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}${WXWIDGETS_CXX_FLAGS}")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}${WXWIDGETS_EXE_LINKER_FLAGS}")

ADD_DEFINITIONS( ${WXWIDGETS_DEFINITIONS} ${PROJECT_DEFINITIONS} )
#
# The include dirs
#
INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR})
INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/common)
# Add project include paths as a separated list
SET(PROJECT_INCLUDE_PATH_LIST ${PROJECT_INCLUDE_PATH})
SEPARATE_ARGUMENTS(PROJECT_INCLUDE_PATH_LIST)
INCLUDE_DIRECTORIES(${PROJECT_INCLUDE_PATH_LIST})
INCLUDE_DIRECTORIES(${WXWIDGETS_INCLUDE_DIR})
INCLUDE_DIRECTORIES(${EXPAT_INCLUDE_DIR})
#
# This is needed to tell CMake what directories contain the libraries we need. This will 
# allow us to just specify the lib files without prepending them with a full path to that
# library
#
LINK_DIRECTORIES( ${WXWIDGETS_LINK_DIRECTORIES} )
# Add project dirs as a separated list
SET(PROJECT_LINK_DIRECTORIES_LIST ${PROJECT_LINK_DIRECTORIES})
SEPARATE_ARGUMENTS(PROJECT_LINK_DIRECTORIES_LIST)
LINK_DIRECTORIES( ${PROJECT_LINK_DIRECTORIES_LIST} )

#tmpled options
# sources of tmpled
SET( PROJECT_SOURCE_FILES sysedmainframe.cpp sysedapp.cpp constredpage.cpp)
SET( PROJECT_SOURCE_FILES ${PROJECT_SOURCE_FILES} tmpledprefdlg.cpp)
#...
SET( PROJECT_SOURCE_FILES ${PROJECT_SOURCE_FILES} ${PROJECT_SOURCE_DIR}/common/titledtextctrl.cpp)
#SET( PROJECT_SOURCE_FILES ${PROJECT_SOURCE_FILES} ${PROJECT_SOURCE_DIR}/common/dirchooser.cpp)

#executable module of tmpled
ADD_EXECUTABLE (tmpled WIN32 ${PROJECT_SOURCE_FILES})
#
# Here we specify what libraries are linked to our project
#
# First, add the WX libs
TARGET_LINK_LIBRARIES(tmpled ${WXWIDGETS_LIBRARIES})
# Then, we add the project specific libs
SET(PROJECT_LIBRARIES_LIST ${PROJECT_LIBRARIES})
# Make the project libs a list so CMake will do the right things
SEPARATE_ARGUMENTS(PROJECT_LIBRARIES_LIST)
TARGET_LINK_LIBRARIES(tmpled ${PROJECT_LIBRARIES_LIST})
TARGET_LINK_LIBRARIES(tmpled ${EXPAT_LIBRARY})
        
Данный проект использует библиотеку wxWidgets Однако для настройки параметров этой библиотеки я использовал не стандартный модуль, а взял другой вот с этого сайта.

Заключение

CMake является довольно мощным генератором Makefile-ов и файлов проектов для IDE. В статье мне удалось лишь намекнуть на ее возможности. Более подробную информацию об использовании этого инструмента можно найти в довольно подробной документации. У буржуинов также вышла бумажная книга об использовании CMake. Ссылки на эту книгу есть на официальном сайте CMake.

Надеюсь, эта статья поможем начинающим программистам в их нелегком пути к совершенству.

Copyleft, 2005 Vadim A. Khohlov aka xvadim (xvadim AT newmail.ru)

1Некоторые редакторы, например vim умеют самостоятельно выставлять при редактировании Makefile табуляции.

2Для ленивых замечу, что нам достаточно зменить не содержимое файла, а лишь дату его модификации. Что можно сделать командой touch.

3Если мне не изменяет мой склероз, первой подобной IDE был Turbo Pascal фирмы Borland.

4"...когда ставки были высокими, души - смелыми, мужчины - настоящими мужчинами, женщины - настоящими женщинами, а маленькие мохнатые зверюшки с Альфа Центавра - настоящими маленькими мохнатыми зверюшками с Альфа Центавра" - читайте Дугласа Адамса :)

5См. Брайн У. Керниган, Роб Пайк "Практика программирования"

6Ну, и потому, что я ее знаю лучше всего (не считая qmake).

7Одно из правил unix-way гласит: "Прежде чем писать какую-то программу, посмотри, а не написал ли кто-то уже нечто подобное." После написания этого модуля, я в Internet нашел подобные модули.