Генерация строк по шаблону в Makefile

Сравнительно недавно при работе с 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. Используемый мной плагин для подсветки синтаксиса затирает символы табуляции.

Ссылки

  1. Функция call: https://www.gnu.org/software/make/manual/html_node/Call-Function.html
  2. Функции word и words: https://www.gnu.org/software/make/manual/html_node/Text-Functions.html
  3. Функция shell: https://www.gnu.org/software/make/manual/html_node/Shell-Function.html
  4. Команда seq: https://man7.org/linux/man-pages/man1/seq.1.html
  5. Функция foreach: https://www.gnu.org/software/make/manual/html_node/Foreach-Function.html

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *