Сравнительно недавно при работе с Makefile я столкнулся с весьма интересной задачей. Над её решением пришлось поломать голову. И сегодня я хотел бы рассказать о самой задаче и о том, как её удалось решить.
Постановка задачи
В одном проекте возникла ситуация, что нам нужно передавать большое количество параметров при компиляции. Выглядело это примерно так:
g++ main.cpp -DAAA=aaa -DBBB=bbb -DCCC=ccc
Когда таких параметров немного — ничего страшного. Но, когда их стало больше 3, перфекционист внутри меня забил тревогу. Нужно что-то придумать.
Хотелось бы иметь что-нибудь вроде этого:
PARAMETERS= AAA BBB CCC VALUES= aaa bbb ccc GENERATED_LINE:=.... all: g++ main.cpp $(GENERATED_LINE) # g++ main.cpp -DAAA=aaa -DBBB=bbb -DCCC=ccc
Весь вопрос в том, как должна выглядеть GENERATED_LINE, чтобы давать нужный результат?
Я мог бы просто опубликовать то решение, которое у меня в итоге получилось. Но я пойду другим путём. Проблема в том, что оно может показаться сложным и непонятным. В нём действительно сложно разобраться, особеннно если Вы (как и я) не являетесь большим экспертом в написании сложных Makefile-ов.
В последующих разделах я отдельно разберу основные его элементы, а потом представлю окончательное решение. Если Вас интересует только оно, то Вы можете сразу перейти к разделу «Окончательное решение».
Также для простоты в последующих примерах я буду опускать вызов компилятора и буду выводить только содержимое переменной GENERATED_LINE.
Решение «в лоб»
Возможное решение «в лоб» может выглядеть так:
GENERATED_LINE= -DAAA=aaa GENERATED_LINE+= -DBBB=bbb GENERATED_LINE+= -DCCC=ccc all: echo $(GENERATED_LINE)
В целом оно работает и решает поставленную задачу. Но хотелось бы чего-нибудь менее «топорного».
Генерация по шаблону
Пообщавшись с ИИ, я узнал, что, оказывается, можно сделать вот так:
TEMPLATE=-D$(1)=$(2) GENERATED_LINE= $(call TEMPLATE,AAA,aaa) GENERATED_LINE+= $(call TEMPLATE,BBB,bbb) GENERATED_LINE+= $(call TEMPLATE,CCC,ccc) all: echo $(GENERATED_LINE)
В строке 1 мы объявляем шаблон, в котором $(1) и $(2) соответствуют параметрам, с которыми вызывается этот шаблон. Для вызова шабона используется встроенная функция call [1].
Это уже шаг вперед. Мы разделили параметры, их значения и шаблон, по которому формируется окончательная строка.
Массивы
В желаемом нами решении использовались массивы параметров и их значений. Для извлечения значений из массивов нам понадобится функция word [2]. Тогда «топорное» решение примет вид:
PARAMETERS= AAA BBB CCC VALUES= aaa bbb ccc GENERATED_LINE= -D$(word 1,$(PARAMETERS))=$(word 1,$(VALUES)) GENERATED_LINE+= -D$(word 2,$(PARAMETERS))=$(word 2,$(VALUES)) GENERATED_LINE+= -D$(word 3,$(PARAMETERS))=$(word 3,$(VALUES)) all: echo $(GENERATED_LINE)
Если применить здесь шаблон из предыдущего раздела, решение примет вид.
PARAMETERS= AAA BBB CCC VALUES= aaa bbb ccc TEMPLATE=-D$(1)=$(2) GENERATED_LINE= $(call TEMPLATE,$(word 1,$(PARAMETERS)),$(word 1,$(VALUES))) GENERATED_LINE+= $(call TEMPLATE,$(word 2,$(PARAMETERS)),$(word 2,$(VALUES))) GENERATED_LINE+= $(call TEMPLATE,$(word 3,$(PARAMETERS)),$(word 3,$(VALUES))) all: echo $(GENERATED_LINE)
Главный недостаток этого решения — явные индексы. Хотелось бы оформить всё это в виде цикла. Но для этого нам нужна последовательность индексов.
Индексы элементов массива
Начнем с длины массива. Для её определения в Makefile используется встроенная функция words [2]. Её работа показана в примере ниже.
PARAMETERS= AAA BBB CCC COUNT:=$(words $(PARAMETERS)) all: echo $(COUNT)
В результате работы этого примера на экран будет выведено число 3.
Значит, нам нужна последовательность чисел от 1 до 3. К сожалению, я не нашел, как сгенерировать её средствами только Makefile. Поэтому решил использовать связку shell [3] и seq [4]. Их совместная работа выглядит так:
PARAMETERS= AAA BBB CCC COUNT:=$(words $(PARAMETERS)) SEQUENCE:=$(shell seq 1 $(COUNT)) all: echo $(SEQUENCE)
В результате работы этого примера на экран будет выведена строка «1 2 3».
Собираем всё вместе
Имея последовательность индексов из предыдущего раздела, мы можем использовать функцию foreach [5]. Объединив это с решением на основе шаблона из раздела «массивы», мы получаем почти окончательное решение:
PARAMETERS= AAA BBB CCC
VALUES= aaa bbb ccc
TEMPLATE=-D$(1)=$(2)
GENERATED_LINE:=$(foreach idx,$(shell seq 1 $(words $(PARAMETERS))), \
$(call TEMPLATE,$(word $(idx),$(PARAMETERS)),$(word $(idx),$(VALUES))))
all:
echo $(GENERATED_LINE)
Это именно то решение, к которому мы и хотели прийти. Да, оно выглядит ужасно, его сложно понять (поэтому выше я и расписывал все его части), но оно решает поставленную задачу. Правда, ровно до тех пор, пока в именах параметров или их значениях не встретится пробельный символ.
Пробельный символ
Главная проблема в случае пробельных символов в том, что make не понимает, что пробельный символ является частью элемента массива. В примере ниже мы рассчитываем на то, что массив содержит 3 элемента, но make упорно считает, что 4.
VALUES= AAA "BB B" CCC all: echo $(words $(VALUES))
Как бы я не изголялся, мне так и не удалось его переубедить. Если нам попадётся такое значение параметра, оно всё сломает. Нужно что-то придумать.
Окончательное решение
Для решения этой проблемы я использовал вспомогательные переменные для значений параметров. Таким образом, окончательное решение приняло вид:
PARAMETERS= AAA BBB CCC
VALUES= VA VB VC
VA=aaa
VB="\"bb b\""
VC=ccc
TEMPLATE=-D$(1)=$($(2))
GENERATED_LINE:=$(foreach idx,$(shell seq 1 $(words $(PARAMETERS))), \
$(call TEMPLATE,$(word $(idx),$(PARAMETERS)),$(word $(idx),$(VALUES))))
all:
echo $(GENERATED_LINE)
# Intermediary result:
# -DAAA=$(VA) -DBBB=$(VB) -DCCC=$(VC)
#
# Output:
# -DAAA=aaa -DBBB="bb b" -DCCC=ccc
Обратите внимание, переменная GENERATED_LINE не изменилась. Изменился только шаблон. Плюс значение каждого параметра вынесено в отдельную переменную.
Работа этого примера основана на рекурсивном раскрытии переменных Makefile.
Недостатки
Приведенный в предыдущем разделе код решает поставленную задачу. Но у него есть несколько недостатков, о которых нужно упомянуть.
- Нет проверки на равенство размеров массивов
PARAMETERSиVALUES. Они должны иметь одинаковое количество элементов. - Утилита
seqесть не везде. Без неё описанное решение работать не будет.
Заключение
Честно говоря, я был удивлен тому, что в make можно проворачивать подобные фокусы. Да, получившееся решение неидеально. Возможно даже существует более простое и элегантное решение (если оно Вам известно — напишите мне). Но оно решает поставленную задачу.
P.S. При копировании примеров статьи нужно заменить пробелы на символ табуляции в рецептах all. Используемый мной плагин для подсветки синтаксиса затирает символы табуляции.
Ссылки
- Функция
call: https://www.gnu.org/software/make/manual/html_node/Call-Function.html - Функции
wordиwords: https://www.gnu.org/software/make/manual/html_node/Text-Functions.html - Функция
shell: https://www.gnu.org/software/make/manual/html_node/Shell-Function.html - Команда
seq: https://man7.org/linux/man-pages/man1/seq.1.html - Функция
foreach: https://www.gnu.org/software/make/manual/html_node/Foreach-Function.html
