qmake: генерация файлов с помощью QMAKE_EXTRA_COMPILERS

Предположим, в проекте есть набор файлов, для которого нужно автоматически генерировать исходные или объектные файлы. Как это можно сделать? Вопрос этот отнюдь не праздный, сам же qmake в стандартной настройке должен уметь запускать препроцессоры Qt (moc, uic, rcc), которые генерируют .cpp и .h файлы для включения в проект.

Для пользователя самым естественным способом использовать препроцессор в qmake будет задать список обрабатываемых файлов в специальной переменной (по аналогии с переменными HEADERS, FROMS и RESOURCES), которые потом будут обработаны автоматически.

Как можно реализовать обработку такой переменной? Если подумать, то ни один из ранее рассмотренных механизмов не годится, поскольку обработка нашей переменной должна происходить в самую последнюю очередь, прямо непосредственно перед формированием Makefile, поскольку фичи должны иметь возможность поменять переменную, и, значит, должны выполняться до ее обработки нашим механизмом.

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

Идея этого механизма заключается в следующем: иметь возможность задать шаблон, по которому qmake будет генерировать цели в Makefile для каждого значения из некоторой переменной. Сами препроцессоры будут вызваны уже при вызове make. Шаблоны можно задавать довольно гибким образом, покрывающим большинство возможных сценариев.

Компилятор ассемблера

Предположим, жизнь заставила заняться извращением — программировать на ассемблере. Для примера возьмем MSVC, в тулчейне которого есть MASM.

В Makefile будут созданы следующего вида правила:

В данном случае, 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}.

.variable_out

Список получившихся исходящих файлов можно сохранить в переменную. Зачем? Для того, чтобы компиляторы можно было выстраивать в цепочки, когда вывод одного компилятора является вводом для другого. Например, если наш генератор кода создает заголовочные файлы, которые потом должны обработаться moc, сделать это можно следующим образом (входящей переменной для moc является HEADERS):

Порядок выполнения компиляторов выбирается таким образом, что, когда выполняется компилятор по некоторой переменной, все компиляторы, которые пишут в эту переменную, уже выполнены. Таким образом, на основании имен переменных для входящих и исходящих файлов компиляторы автоматически выстраиваются в «конвейер» по обработке файлов в несколько этапов.

Если переменная вывода не указана, то она полагается равной OBJECTS, и в этом случае файлы будут автоматически прилинкованы. Если это лишнее, можно просто указать переменную вывода. Также можно отключить линковку явно следующим образом:

Переменная SOURCES обрабатывается специальным образом. Компиляторы не могут добавлять в нее результаты своей работы. Если указать SOURCES в .variable_out, файлы будут добавлены в переменную GENERATED_SOURCES.

Если результирующие файлы не используются другими компиляторами и не являются объектными файлами в переменной OBJECTS, то, хоть цели по созданию этих файлов и будут в Makefile, но при обычном построении выполнены они не будут, т.к. они не встречаются в зависимостях обрабатываемых целей. Как вариант, можно добавить эти файлы в PRE_TARGETDEPS. Обычно это именно то, что нужно, поэтому предусмотрен способ сделать это быстро:

Зависимости

qmake автоматически добавляет исходные файлы как зависимости к исходящим. Часто этого достаточно, но если нет, то можно указать зависимости самостоятельно, причем двумя способами.

.depends и .depend_command имеют важное различие: первый способ позволяет указать несуществующие файлы, и они будут добавлены в зависимости, в то время как при использовании .depend_command несуществующие файлы теряются.

Полезное применение для .depends — указать скрипт/программу, которая используется для формирования файлов, как зависимость. В этом случае, если программа-генератор поменяется, то все проекты, ее использующие, учтут этот факт при построении и перегенерируют файлы.

Во избежание лишних знакомств с граблями, в .depend_command используйте полный путь к запускаемой программе.

Если исходные файлы имеют расширения C/C++ файлов (.c, .h, .cpp, …), то qmake делает доброе дело и парсит эти файлы на пример дополнительных зависимостей (т.е. включенных файлов) и добавляет их как зависимости. Если исходные файлы являются C/C++ файлами, но имеют нетрадиционные расширения, то эту фичу можно включить с помощью явного указания типа исходных файлов:

Все рассмотренные способы добавления зависимостей работают независимо друг от друга. В частности, явное указание зависимостей не мешает формированию неявных зависимостей. Если нужен полный контроль над происходящим, можно воспользоваться опцией explicit_dependencies. В этом случае зависимости будут взяты только из .depends и ниоткуда более, даже переменная .depend_command будет проигнорирована.

clean

По умолчанию сгенерированные компилятором файлы автоматически добавляются в clean. Обычно этого достаточно, но если нужно, можно очистку поменять. Для примера предположим, наш препроцессор создает .cpp файлы, и для каждого из них еще и временный с расширением .tmp. Последние тоже неплохо бы чистить.

Использование .clean отключает автоматический механизм, так что об очистке исходящих файлов нужно будет позаботиться самостоятельно.

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

combine

Рассмотренный выше механизм QMAKE_EXTRA_COMPILERS для каждого входящего файла формирует ровно один исходящий. Но иногда полезно уметь на основании нескольких входящих файлов сформировать один исходящий. qmake позволяет это сделать следующим образом:

Исходящий файл при этом будет зависеть от всех входящих согласно тем же правилам, что были рассмотрены выше.

QMAKE_VAR_*

В команде, кончено, можно использовать переменные qmake. Но на момент формирования команды эта переменная может еще не иметь полного значения! Она может быть изменена фичами и компиляторами, и эти изменения учтены не будут. Для таких случаев предусмотрен следующий механизм: выражения вида ${QMAKE_VAR_varname} будут заменены на значения соответствующих переменных varnameво время отрабатывания механизма QMAKE_EXTRA_COMPILERS.

.verify_function

Можно проверить, валидного ли вида файлы даются на вход компилятору/препроцессору. qmake позволяет сделать так, чтобы для исходных файлов была вызвана условная функция пользователя, и правило в Makefile будет сформировано только для прошедших проверку файлов.

В Makefile будет создано правило только для abc.txt, правила для test.txt и test2.txt не пройдут проверку.

Важный момент: вся эта кухня отрабатывает только в том случае, если исходные файлы существуют. Если некий файл из списка не существует, то он считается прошедшим проверку и правило для него сформировано будет. Тестовые функции из verify_function для несуществующего файла даже не вызываются.

ignore_no_exist

Правила создаются для всех входящих файлов, независимо от того, существуют они или нет. Для не найденных файлов qmake выдаст предупреждение, но этим и ограничится. Если такое поведение не подходит по задаче, то можно сделать так, чтобы для несуществующих входящих файлов правила не формировались:

Цикл: qmake

Добавить комментарий