|
|||||||
Как работать с Makefile-проектами в среде CLion
Время создания: 18.05.2022 10:20
Автор: Andrey Shcheglov
Текстовые метки: linux, make, проект, сборка, clion
Раздел: Компьютер - Программирование - Системы сборки - Make
Запись: xintrea/mytetra_syncro/master/base/165285844217o74eohi8/text.html на raw.github.com
|
|||||||
|
|||||||
За последние несколько лет мне пришлось столкнуться с множеством вопросов, которые были сформулированы примерно так: «мой проект не открывается в среде CLion». В свою очередь, это приводило к необходимости из раза в раз объяснять разным людям примерно одно и то же. Статья имеет целью сохранить тот опыт, который был накоплен в процессе анализа десятков разных проектов. CLion — проприетарная кроссплатформенная среда разработки для языков C и C++ от компании JetBrains. Предполагается, что официальная документация вам уже известна и не вызывает вопросов. Если вам лень вникать в скучные технические детали, можете перейти прямо к разделу «Рекомендации». Постановка задачиОсновная проблема с проектами, использующими в качестве системы сборки Make, состоит в том, что эта система не предоставляет ровным счётом никакой информации о проектной модели, т. е. о том, какие файлы исходного кода попадут на вход компилятора, какие ключи компилятора, директивы препроцессора и заголовочные файлы будут использованы, и какие бинарные файлы мы получим на выходе. Эта информация остаётся неизвестной до тех пор, пока проект не будет собран. В этом состоит сложность задачи интеграции сред разработки (IDE) и системы сборки Make. Рассмотрим, например, проект с вот такой плоской структурой:
и вот таким элементарным Makefile: .PHONY: all all: foo .PHONY: clean clean: $(RM) foo Здесь видно, что файл foo.c является частью проектной модели, т. к. будет участвовать в процессе компиляции (посредством встроенного правила $(CC) foo.c -o foo), а файл bar.c — не является. Подходы к анализу проектной моделиБаза данных компиляцииМожно решить задачу «в лоб» и сначала собрать проект, а уж затем выяснить, какие файлы и с какими флагами были скомпилированы. Для создания файла compile_commands.json (собственно базы данных компиляции) будем использовать любой из доступных генераторов — bear или compiledb . bear работает посредством перехвата динамических вызовов (LD_PRELOAD) и потому выдаёт достаточно точный результат, не зависит от системы сборки (т. е. может использоваться совместно с любой системой сборки, хоть с чёртом в ступе, а не только с Make), но имеет ограничения на Mac OS X и вообще не работает на Windows. С другой стороны, compiledb анализирует исключительно вывод команды make и потому нередко совершает ошибки, но, с другой стороны, работает везде. Если вы используете Linux, я предлагаю вам остановить свой выбор именно на bear. По крайней мере, у вас не будет ошибок, связанных с неверной интерпретацией двойных кавычек, апострофов и путей, содержащих пробелы. Итак, мы собрали наш проект, «обернув» команду сборки и выполнив что-то вроде bear make или bear make all, и теперь имеем на выходе заветный compile_commands.json. CLion вполне в состоянии открыть этот файл как проект, но у такого подхода есть по меньшей мере 2 недостатка:
Поэтому стоит задаться вопросом, нет ли способа проанализировать структуру проекта, не выполняя сборку. Переопределение переменных окруженияПереопределение переменной $(MAKE)Этот подход не может использоваться как самостоятельное решение, однако, если проект использует рекурсивные вызовы Make (см. 5.7 Recursive Use of make ), можно переопределить переменную MAKE и, подставив в значение путь до собственной «обёртки» над Make (MAKE=my-custom-make-wrapper), отслеживать все такие вызовы и, при необходимости, менять или дополнять флаги Make, редактируя аргументы перехватываемой командной строки. Переопределение компиляторовПереопределяя переменные CC и CXX (и имея «обёртку», которая может имитировать поведение компилятора), можно перехватывать вызовы компилятора и, таким образом, точно знать, в каком каталоге и с какими аргументами компилятор был вызван. Переопределение оболочкиПереопределяя переменную SHELL (при наличии «обёртки», опять же), можно получить информацию обо всех вызываемых командах (а не только о командах компиляции). Разумеется, вышеупомянутые техники можно произвольным образом комбинировать. Перехват системных вызововНа Linux, Solaris и, с оговорками, Mac OS X информацию о системных вызовах execve() (и интересующих нас аналогах) можно получить через механизм LD_PRELOAD или (Linux) запуская утилиту strace . Впрочем, полученное решение уже не будет кроссплатформенным. Мне не известен ни один инструмент, где бы хотя бы частично были реализованы эти техники, кроме, быть может, NetBeans CND и Oracle Solaris Studio. Однако, насколько мне известно, поддержка Makefile-проектов в упомянутых продуктах не развивалась с 2016 года. Запуск Make в режиме «dry run»На русский язык переводится как «репетиция», но подобного русскоязычного термина попросту нет, поэтому впредь будем называть этот режим именно «dry run», как в англоязычной документации. Вот описание ключа командной строки GNU Make: -n, –just-print, –dry-run, –recon Print the commands that would be executed, but do not execute them (except in certain circumstances). Помимо этого флага, нам ещё понадобится флаг w: -w, –print-directory Print a message containing the working directory before and after other processing. This may be useful for tracking down errors from complicated nests of recursive make commands. и флаг k: -k, –keep-going Continue as much as possible after an error. While the target that failed, and those that depend on it, cannot be remade, the other dependencies of these targets can be processed all the same. Полная командная строка, таким образом, будет make -wnk, и вывод команды Make в большинстве случаев позволяет нам проанализировать структуру проекта. Этот способ не столь точен, как переопределение переменных или перехват системных вызовов, но он относительно прост в реализации, и в CLion используется именно этот подход. Вручную указывать в настройках проекта флаги w, n и k не нужно: в процессе анализа проектной модели CLion подставит их автоматически. При необходимости глобальные значения флагов, используемых для анализа, можно изменить в расширенных настройках, но доля проектов, где в этом была бы практическая необходимость, исчезающе мала: снимок . Выделение списка целейПомимо анализа проектной модели, CLion умеет собирать информацию о том, какие цели объявлены в Makefile. Каждая выявленная цель автоматически становится конфигурацией типа Makefile Application : снимок . GNU MakeДля GNU Make информация о целях собирается так же, как это сделано в соответствующем сценарии из проекта bash-completion , достаточно воспользоваться флагом p: -p, –print-data-base Print the data base (rules and variable values) that results from reading the makefiles; then execute as usual or as otherwise specified. This also prints the version information given by the -v switch. To print the data base without trying to remake any files, use make -p -f/dev/null. и выполнить make -npq. BSD MakeЗдесь нужен другой подход: BSD Make ничего не знает о флаге p, это расширение GNU. В настоящее время CLion не поддерживает BSD Make, но, чисто теоретически, «научить» работать с целями BSD Make достаточно просто, используя (опять же, нестандартный) флаг V: -V variable Print bmake’s idea of the value of variable, in the global context. Do not build any targets. Multiple instances of this option may be specified; the variables will be printed one per line, with a blank line for each null or undefined variable. If variable contains a ‘$’ then the value will be expanded before printing. Таким образом, список целей можно легко получить, выполнив команду bmake V '$(.ALLTARGETS)' Если хочется исключить из этого списка синтетические «псевдоцели» (.WAIT), команду надо привести к следующему виду: bmake -V '$(.ALLTARGETS:N.WAIT_*:O)' РекомендацииТеперь, собственно, то, ради чего вся статья и была написана. Следование этим рекомендациям не даст стопроцентной гарантии, что ваш проект без ошибок откроется в CLion, но, во всяком случае, существенно снизит количество этих ошибок. Часть советов касается рекурсивных вызовов Make. Внимательный читатель, вероятно, скажет, что это абсолютное зло, сославшись на статью Питера Миллера «Recursive Make Considered Harmful» (HTML , PDF ). На что можно возразить, что подавляющее большинство проектов, основанных на GNU Autotools, таки использует рекурсию Make, так что зло это хоть и абсолютное, но, увы, неизбежное. К тому же, как выяснилось в процессе подготовки статьи, есть и альтернативный взгляд на вещи . Начнём.
Первая причина никак не связана собственно с CLion: у кого-то из пользователей инструмент Make может отсутствовать в переменной PATH или быть установлен как gmake или bmake. Рекурсивно вызывая $(MAKE), вы можете быть уверены, что для родительского и дочернего процессов Make будет использован один и тот же исполняемый файл (напр., /usr/bin/make), т. е., скажем, GNU Make никогда не породит BSD Make, и наоборот. Во-вторых, в пресловутом режиме «dry run», используемом для анализа проектной модели, первая форма записи будет распознана как рекурсивный вызов с печатью соответствующих команд, а вторая — нет. Рассмотрим проект с такой структурой: А теперь сравним два варианта вывода Make. Первый, через $(MAKE): make: Entering directory '/home/alice' make -C foo all make[1]: Entering directory '/home/alice/foo' echo "Making all in foo..." gcc -c -o foo.o foo.c make[1]: Leaving directory '/home/alice/foo' make -C bar all make[1]: Entering directory '/home/alice/bar' echo "Making all in bar..." gcc -c -o bar.o bar.c make[1]: Leaving directory '/home/alice/bar' make: Leaving directory '/home/alice' А теперь make: make: Entering directory '/home/alice' make -C foo all make -C bar all make: Leaving directory '/home/alice' Во втором случае, если в каком-то из дочерних (рекурсивно вызываемых) Makefile’ов был столь нужный нам вызов компилятора, мы этого просто не увидим. Кстати, ровно в вышеописанном нюансе состоит сложность реализации поддержки средой CLion инструмента BSD Make: с точки зрения конечного пользователя, bmake -wnk никогда не распознаёт рекурсивные вызовы, независимо от формы записи. Связано это с тем, что GNU Make в режиме «dry-run» (-n) для каждого рекурсивного исполнения $(MAKE) производит системный вызов execve() (тоже с флагом -n, разумеется), а вот BSD Make — как раз нет (разница легко обнаруживается при запуске утилиты strace с ключом -f): $ strace -f -e execve make -wnk 2>&1 >/dev/null | grep -vF ENOENT | grep -F execve execve("/usr/bin/make", ["make", "-wnk"], 0x7ffe8a5a35a0 /* 80 vars */) = 0 [pid 15729] execve("/usr/bin/make", ["make", "-C", "foo", "all"], 0x5608f4544a30 /* 84 vars */) = 0 [pid 15730] execve("/usr/bin/make", ["make", "-C", "bar", "all"], 0x5608f4544a30 /* 84 vars */) = 0 $ strace -f -e execve bmake -wnk 2>&1 >/dev/null | grep -vF ENOENT | grep -F execve execve("/usr/bin/bmake", ["bmake", "-wnk"], 0x7ffc10221bb0 /* 80 vars */) = 0 Вот так плохо: foo.o: foo.c cc -c -o $@ $< Вот так хорошо: foo.o: foo.c $(CC) -c -o $@ $< GNUMAKEFLAGS += --no-print-directory В качестве альтернативы, если вы работаете с «чужим» проектом, куда у вас нет прав на запись (пусть, напр., Node.js ), и не хотите менять файлы, находящиеся под контролем системы VCS, можно для фазы анализа включить флаг e: -e, –environment-overrides Give variables taken from the environment precedence over variables from makefiles. Вот так могут выглядеть настройки проекта для Node.js: снимок . Включение флага e через поле «Arguments» может быть альтернативным решением и в иных случаях, когда на уровне Makefile переопределены флаги Make или другие стандартные переменные окружения (см. ниже). MAKEFLAGS += j8 .PHONY: all all: foo-all bar-all .PHONY: foo-all foo-all: $(MAKE) -C foo all .PHONY: bar-all bar-all: $(MAKE) -C bar all В таких условиях Make будет использовать более одного (в примере выше — 8) параллельного процесса при рекурсивных вызовах, в результате чего в выводе команды сообщения вида «Entering directory ‘…’» и «Leaving directory ‘…’» будут перемешаны между собой, команды компиляции — произвольным образом разбросаны между этими сообщениями, и CLion не сможет отследить ни смену каталога, ни принадлежность команды тому или иному каталогу: make: Entering directory '/home/alice' make -C foo all make -C bar all make[1]: Entering directory '/home/alice/foo' make[1]: Entering directory '/home/alice/bar' echo "Making all in foo..." make[1]: Leaving directory '/home/alice/foo' echo "Making all in bar..." make[1]: Leaving directory '/home/alice/bar' make: Leaving directory '/home/alice' С учётом вышесказанного, если вы хотите иметь возможность собирать проект, используя несколько параллельных процессов Make, передайте флаг -j через поле «Build options» в настройках проекта (но ни в коем случае не через поле «Arguments» — флаги в этом поле используются для анализа проектной модели, но не для сборки): снимок . export LC_ALL = ru_RU.UTF-8 export LANG = ru_RU.UTF-8 .PHONY: all all: foo-all bar-all .PHONY: foo-all foo-all: $(MAKE) -C foo all .PHONY: bar-all bar-all: $(MAKE) -C bar all Вывод команды make -wnk: make: Entering directory '/home/alice' make -C foo all make[1]: вход в каталог «/home/alice/foo» echo "Making all in foo..." make[1]: выход из каталога «/home/alice/foo» make -C bar all make[1]: вход в каталог «/home/alice/bar» echo "Making all in bar..." make[1]: выход из каталога «/home/alice/bar» make: Leaving directory '/home/alice' target: cd subdir && $(MAKE) target использовать форму target: $(MAKE) -C subdir target Помимо служебных сообщений Make («Entering directory», «Leaving directory»), CLion, безусловно, обработает и команду cd и отследит смену текущего каталога, но второй вариант записи кажется надёжнее. и вот таким Makefile’ом в корневом каталоге проекта: all-recursive: for subdir in foo bar baz; \ do (cd $$subdir; $(MAKE) all) || exit 1; done Здесь рецепт цели all-recursive — это цикл оболочки POSIX, который, скорее всего, не будет выполнен в режиме «dry run». Если воспользоваться функцией $(foreach), то можно переписать так: all-recursive: $(foreach subdir,$(SUBDIRS),$(MAKE) -C $(subdir) all;) all: $(CC) -c *.c Если в одном с Makefile’ом каталоге находятся, например, файлы foo.c и bar.c, то на стадии анализа команда make -wnk по-прежнему выведет cc -c *.c а CLion не умеет вычислять маски оболочки (к тому же, на UNIX и Windows оболочки разные и, соответственно, синтаксис масок слегка различен). Вот так хорошо: all: $(CC) -c $(wildcard *.c) В этом случае значение маски будет вычислено средствами Make: cc -c foo.c bar.c Файлы исходного кода должны существовать в дереве проекта на момент анализа проектной модели. main.o: main.cpp $(CXX) -I../ -g -Wall $(shell pkg-config some-library) -c -o $@ $< в то время как такой — нет: main.o: main.cpp $(CXX) -I../ -g -Wall `pkg-config some-library` -c -o $@ $< $(CXX) -I../ -g -Wall $$(pkg-config some-library) -c -o $@ $< Это связано с тем, что CLion, за вычетом некоторых исключений (напр., VPATH -сборки в GNU Autotools), не умеет анализировать командные строки, использующие вложенную оболочку. На этом всё. Надеюсь, описанный опыт был кому-то полезен. Есть ещё некоторые нюансы, которые проще всего проиллюстрировать на конкретном Makefile-проекте (а именно, ядре Linux). |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|