Предположим, в проекте есть набор файлов, для которого нужно автоматически генерировать исходные или объектные файлы. Как это можно сделать? Вопрос этот отнюдь не праздный, сам же qmake в стандартной настройке должен уметь запускать препроцессоры Qt (moc, uic, rcc), которые генерируют .cpp и .h файлы для включения в проект.
Для пользователя самым естественным способом использовать препроцессор в qmake будет задать список обрабатываемых файлов в специальной переменной (по аналогии с переменными HEADERS
, FROMS
и RESOURCES
), которые потом будут обработаны автоматически.
Как можно реализовать обработку такой переменной? Если подумать, то ни один из ранее рассмотренных механизмов не годится, поскольку обработка нашей переменной должна происходить в самую последнюю очередь, прямо непосредственно перед формированием Makefile, поскольку фичи должны иметь возможность поменять переменную, и, значит, должны выполняться до ее обработки нашим механизмом.
Поэтому был реализован механизм QMAKE_EXTRA_COMPILERS
, который отрабатывает как раз в нужное время — после фич. С помощью этого средства реализованы стандартные препроцессоры Qt (переменная FORMS
обрабатывается uic и т.п.), и ничто не мешает подобным образом реализовать свои препроцессоры и компиляторы.
Идея этого механизма заключается в следующем: иметь возможность задать шаблон, по которому qmake будет генерировать цели в Makefile для каждого значения из некоторой переменной. Сами препроцессоры будут вызваны уже при вызове make. Шаблоны можно задавать довольно гибким образом, покрывающим большинство возможных сценариев.
Компилятор ассемблера
Предположим, жизнь заставила заняться извращением — программировать на ассемблере. Для примера возьмем MSVC, в тулчейне которого есть MASM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# переменная для исходных файлов ASM += one.asm two.asm # Просто описание компилятора, # не несет функциональной нагрузки. masm.name = MASM compiler # Имя переменной для исходных файлов. # Для каждого из них будет сформировано # правило в Makefile masm.input = ASM # Имя исходящего файла. # Имя цели в правиле будет иметь именно этот вид. # Что такое ${QMAKE_FILE_BASE} - ниже. masm.output = ${QMAKE_FILE_BASE}.obj # команды для формирования исходящего файла # в данном случае - вызов MASM 32bit masm.commands = ml /Fo ${QMAKE_FILE_OUT} /c ${QMAKE_FILE_IN} # добавляем компилятор в список препроцессоров QMAKE_EXTRA_COMPILERS += masm |
В Makefile будут созданы следующего вида правила:
1 2 3 4 5 6 7 |
one.obj: one.asm ml /Fo one.obj /c one.asm two.obj: two.asm ml /Fo two.obj /c two.asm |
В данном случае, one.obj и two.obj автоматически добавлены в переменную Makefile OBJECTS
и, как следствие, прилинковываются без дополнительных действий со стороны программиста. Подробности ниже. Также обратите внимание: исходный файл (.asm в данном случае) автоматически добавляется как зависимость для исходящего файла (.obj в этом примере).
Метки-заполнители
Шаблон задается с помощью набора «переменных» специального вида, которые служат метками-заполнителями (placeholders). Значения для меток вычисляются на основании обрабатываемого значения из исходной переменной. Понимаются следующие метки:
${QMAKE_FILE_IN}
— значение из исходной переменной, которое обычно — имя файла. Далее буду называть входящим файлом.${QMAKE_FILE_IN_BASE}
или${QMAKE_FILE_BASE}
— имя входящего файла без расширения (и без пути).${QMAKE_FILE_IN_PATH}
или${QMAKE_FILE_PATH}
— каталог входящего файла. Путь не преобразуется в абсолютный. Не содержит / в конце пути.${QMAKE_FILE_EXT}
— расширение входящего файла, включая точку.${QMAKE_FILE_OUT}
— вычисленное (на основании переменной .output) имя исходящего файла, т.е. файла, который будет сгенерирован на основании входящего файла.${QMAKE_FILE_OUT_BASE}
— имя исходящего файла без расширения и пути.${QMAKE_FILE_OUT_PATH}
— каталог исходящего файла.
Если этих заполнителей недостаточно, можно извернуться одним из следующих способов:
- Выражение
${QMAKE_FUNC_FILE_IN_func}
будет заменено на результат вызова функции с именемfunc
, которая должна быть определена с помощьюdefineReplace
. Функция получает один параметр, равный${QMAKE_FILE_IN}
. - Выражение
${QMAKE_FUNC_func}
будет заменено на результат вызова функции с именемfunc
, которая должна быть определена с помощьюdefineReplace
. Функция получает два параметра, равные${QMAKE_FILE_IN}
и${QMAKE_FILE_OUT}
.
Также можно выкрутиться, если функционала .output
для формирования имени исходящего файла недостаточно. Вместо .output
можно указать .output_function
, в которой указывается имя пользовательской функции. Эта функция получает ${QMAKE_FILE_IN}
и должна вернуть значение, которому будет равен ${QMAKE_FILE_OUT}
.
1 2 3 4 5 6 7 8 |
defineReplace(outname) { return($${1}.txt) } ... test.output_function = outname |
.variable_out
Список получившихся исходящих файлов можно сохранить в переменную. Зачем? Для того, чтобы компиляторы можно было выстраивать в цепочки, когда вывод одного компилятора является вводом для другого. Например, если наш генератор кода создает заголовочные файлы, которые потом должны обработаться moc, сделать это можно следующим образом (входящей переменной для moc является HEADERS
):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
GENERATOR += 1.gen 2.gen generator.name = my generator generator.input = GENERATOR generator.output = ${QMAKE_FILE_BASE}.h generator.commands = generate ${QMAKE_FILE_IN} ${QMAKE_FILE_OUT} # переменная, в которую будет добавлен список исходящих файлов generator.variable_out = HEADERS QMAKE_EXTRA_COMPILERS += generator |
Порядок выполнения компиляторов выбирается таким образом, что, когда выполняется компилятор по некоторой переменной, все компиляторы, которые пишут в эту переменную, уже выполнены. Таким образом, на основании имен переменных для входящих и исходящих файлов компиляторы автоматически выстраиваются в «конвейер» по обработке файлов в несколько этапов.
Если переменная вывода не указана, то она полагается равной OBJECTS
, и в этом случае файлы будут автоматически прилинкованы. Если это лишнее, можно просто указать переменную вывода. Также можно отключить линковку явно следующим образом:
1 2 3 4 |
# переменная .CONFIG служит для указания различных опций generator.CONFIG += no_link |
Переменная SOURCES
обрабатывается специальным образом. Компиляторы не могут добавлять в нее результаты своей работы. Если указать SOURCES
в .variable_out
, файлы будут добавлены в переменную GENERATED_SOURCES
.
Если результирующие файлы не используются другими компиляторами и не являются объектными файлами в переменной OBJECTS, то, хоть цели по созданию этих файлов и будут в Makefile, но при обычном построении выполнены они не будут, т.к. они не встречаются в зависимостях обрабатываемых целей. Как вариант, можно добавить эти файлы в PRE_TARGETDEPS
. Обычно это именно то, что нужно, поэтому предусмотрен способ сделать это быстро:
1 2 3 4 |
# исходящие файлы будут добавлены в PRE_TARGETDEPS generator.CONFIG += target_predeps |
Зависимости
qmake автоматически добавляет исходные файлы как зависимости к исходящим. Часто этого достаточно, но если нет, то можно указать зависимости самостоятельно, причем двумя способами.
1 2 3 4 5 6 7 8 9 |
# список имен файлов, которые будут добавлены в зависимости # можно использовать метки-заполнители generator.depends = ${QMAKE_FILE_BASE}.txt ${QMAKE_FILE_BASE}.cmd # stdout указанной команды интерпретируется как список зависимостей generator.depend_command += echo main.h # файл main.h должен существовать |
.depends
и .depend_command
имеют важное различие: первый способ позволяет указать несуществующие файлы, и они будут добавлены в зависимости, в то время как при использовании .depend_command
несуществующие файлы теряются.
Полезное применение для .depends
— указать скрипт/программу, которая используется для формирования файлов, как зависимость. В этом случае, если программа-генератор поменяется, то все проекты, ее использующие, учтут этот факт при построении и перегенерируют файлы.
Во избежание лишних знакомств с граблями, в .depend_command
используйте полный путь к запускаемой программе.
Если исходные файлы имеют расширения C/C++ файлов (.c, .h, .cpp, …), то qmake делает доброе дело и парсит эти файлы на пример дополнительных зависимостей (т.е. включенных файлов) и добавляет их как зависимости. Если исходные файлы являются C/C++ файлами, но имеют нетрадиционные расширения, то эту фичу можно включить с помощью явного указания типа исходных файлов:
1 2 3 4 5 6 7 8 |
# исходящие файлы будут интерпретированы как C/C++ файлы generator.dependency_type = TYPE_C # есть еще один тип файлов, которые qmake умеет парсить # на предмет зависимостей - .ui, xml-файлы форм. generator.dependency_type = TYPE_UI |
Все рассмотренные способы добавления зависимостей работают независимо друг от друга. В частности, явное указание зависимостей не мешает формированию неявных зависимостей. Если нужен полный контроль над происходящим, можно воспользоваться опцией explicit_dependencies
. В этом случае зависимости будут взяты только из .depends
и ниоткуда более, даже переменная .depend_command
будет проигнорирована.
1 2 3 4 |
# зависимости только из .depends generator.CONFIG += explicit_dependencies |
clean
По умолчанию сгенерированные компилятором файлы автоматически добавляются в clean. Обычно этого достаточно, но если нужно, можно очистку поменять. Для примера предположим, наш препроцессор создает .cpp файлы, и для каждого из них еще и временный с расширением .tmp. Последние тоже неплохо бы чистить.
1 2 3 4 5 6 7 8 |
generator.output = ${QMAKE_FILE_BASE}.cpp # Список файлов на удаление. # Если используются шаблоны, то список удаляемых будет формироваться # и добавляться в clean для каждого входящего файла generator.clean = ${QMAKE_FILE_OUT} ${QMAKE_FILE_BASE}.tmp |
Использование .clean
отключает автоматический механизм, так что об очистке исходящих файлов нужно будет позаботиться самостоятельно.
Также можно добавить в очистку произвольную команду. Если команда не включает в себя шаблоны, то она добавится в единственном экземпляре, а если включает, то добавится по команде для каждого входящего файла:
1 2 3 4 5 |
# пустая команда без использования шаблонов для отключения стандартного механизма generator.clean = @echo off generator.clean_commands = -$(DEL_FILE) ${QMAKE_FILE_OUT} && @echo ${QMAKE_FILE_OUT} deleted! |
combine
Рассмотренный выше механизм QMAKE_EXTRA_COMPILERS
для каждого входящего файла формирует ровно один исходящий. Но иногда полезно уметь на основании нескольких входящих файлов сформировать один исходящий. qmake позволяет это сделать следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 |
test.CONFIG += combine test.CONFIG += no_link test.input = HEADERS test.name = combine testing # метки-заполнители не используются test.output = output.txt # ${QMAKE_FILE_IN} будет заменен списком имен входящих файлов test.commands = echo ${QMAKE_FILE_IN} > ${QMAKE_FILE_OUT} |
Исходящий файл при этом будет зависеть от всех входящих согласно тем же правилам, что были рассмотрены выше.
QMAKE_VAR_*
В команде, кончено, можно использовать переменные qmake. Но на момент формирования команды эта переменная может еще не иметь полного значения! Она может быть изменена фичами и компиляторами, и эти изменения учтены не будут. Для таких случаев предусмотрен следующий механизм: выражения вида ${QMAKE_VAR_varname}
будут заменены на значения соответствующих переменных varname
— во время отрабатывания механизма QMAKE_EXTRA_COMPILERS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# в какой-нибудь фиче, например GGG = aaa bbb ccc .... # в 1.txt будет записано # aaa bbb ccc test.commands = echo ${QMAKE_VAR_GGG} > 1.txt # в 1.txt будет записано # только первое значение из переменной, aaa test.commands = echo ${QMAKE_VAR_FIRST_GGG} > 1.txt QMAKE_EXTRA_COMPILERS += test |
.verify_function
Можно проверить, валидного ли вида файлы даются на вход компилятору/препроцессору. qmake позволяет сделать так, чтобы для исходных файлов была вызвана условная функция пользователя, и правило в Makefile будет сформировано только для прошедших проверку файлов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# файлы должны существовать HEADERS += \ test.h \ test2.h \ abc.h # возвращает истину, если имя входящего файла не содержит test.h defineTest(comp_check1) { # первый параметр - исходящий файл out_file = $$1 # второй параметр - входящий файл in_file = $$2 message(comp_check1: $$out_file | $$in_file) substr = $$find(in_file, test.h) message($$substr) isEmpty(substr) { return(true) } else { return(false) } } # возвращает истину, если имя входящего файла содержит test2.h defineTest(comp_check2) { out_file = $$1 in_file = $$2 message(comp_check2: $$out_file | $$in_file) substr = $$find(in_file, test2.h) !isEmpty(substr) { return(true) } else { return(false) } } test.input = HEADERS test.output = ${QMAKE_FILE_BASE}.txt test.name = test of verify_function test.commands = echo ${QMAKE_FILE_IN} > ${QMAKE_FILE_BASE}.txt test.CONFIG += no_link # включаем механизм verify_function test.CONFIG += function_verify # список условных функций, можно с отрицанием test.verify_function = comp_check1 !comp_check2 QMAKE_EXTRA_COMPILERS += test |
В Makefile будет создано правило только для abc.txt, правила для test.txt и test2.txt не пройдут проверку.
Важный момент: вся эта кухня отрабатывает только в том случае, если исходные файлы существуют. Если некий файл из списка не существует, то он считается прошедшим проверку и правило для него сформировано будет. Тестовые функции из verify_function
для несуществующего файла даже не вызываются.
ignore_no_exist
Правила создаются для всех входящих файлов, независимо от того, существуют они или нет. Для не найденных файлов qmake выдаст предупреждение, но этим и ограничится. Если такое поведение не подходит по задаче, то можно сделать так, чтобы для несуществующих входящих файлов правила не формировались:
1 2 3 |
test.CONFIG += ignore_no_exist |