>
>
Как мы исправили один баг в CMake

Сергей Ларин
Статей: 13

Как мы исправили один баг в CMake

В августе 2019 года в CMake появилась долгожданная поддержка предкомпилированных заголовков. До этого приходилось использовать разные плагины, например, Cotire. Сразу после выпуска CMake с новым функционалом было ещё несколько доработок. Но уже осенью мы посчитали, что можно начать использовать эту фичу, и переписали наши скрипты. Позже мы нашли баг, который формировал неправильные параметры компилятора Clang и мешал запуску анализатора PVS-Studio. Баг пришлось исправить самим.

Что не так?

Проблема была в том, что исходный файл, в котором использовался предкомпилированный заголовок, был некорректно препроцессирован, если использовался компилятор Clang. На месте header-файла было пустое место. Но в GCC и MSVC все работало нормально.

Что делает CMake?

Стоит рассказать о том, как CMake формирует предкомпилированный заголовок. Для каждого target'а (а начиная с CMake 3.17 и для каждого типа сборки – Debug, Release, ...) создается 2 файла: cmake_pch.hxx и cmake_pch.cxx. hxx-файл – это header-файл, из которого уже будет сформирован предкомпилированный заголовок, а cxx – пустой файл с содержимым:

/* Generated by CMake */

Нужен он для генерации предкомпилированного заголовка – он используется как исходник при запуске компилятора.

Во время сборки проекта заголовочный файл включается через передачу флагов компилятора. Для Clang это:

-Xclang -include-pch -Xclang <PCH_FILE>

Т.е. в команду компиляции передается уже созданный предкомпилированный заголовок. В итоге команды запуска сохраняются в файле compile_commands.json, который анализатор использует для проверки проекта.

Проблема

Однако на этом моменте имя оригинального header-файла потеряно. При запуске препроцессора Clang пытается достать имя заголовочного файла из созданного предкомпилированного заголовка и в итоге использует пустой файл cmake_pch.cxx. Почему? Если взглянуть в документацию Clang, то можно увидеть их способ создания предкомпилированных заголовков:

To generate PCH files using clang -cc1, use the option -emit-pch:
$ clang -cc1 test.h -emit-pch -o test.h.pch

Заголовочный файл test.h используется в качестве исходного. А вот как делает CMake:

-Xclang -emit-pch -Xclang -include -Xclang <PCH_HEADER>

Он включает его через специальный флаг, а затем передает cmake_pch.cxx как исходник. Информация о включенных файлах, переданных через командную строку, к сожалению, теряется, а информация об исходном файле остается. Это можно узнать, посмотрев документацию Clang:

Original file name
    The full path of the header that was used to generate the AST file.

Но как в этом случае работал Cotire?

Cotire по своей структуре похож на CMake, однако cxx-файл выглядел примерно так:

#ifdef __cplusplus
#include "/path/to/header.hxx"
#endif

В исходнике у него была директива include, поэтому препроцессор работал нормально. Это первое, что мы хотели предложить в качестве исправления.

Однако при таком подходе появились проблемы - нарушалась кодировка файлов (компилятор может преобразовывать исходный файл в свою кодировку), могла сломаться работа других компиляторов, а в CMake 3.17 файл cmake_pch.cxx создавался только один для каждого типа сборки, когда hxx-файлы могли отличаться.

Решение

В итоге было решено передавать в строку компиляции еще и оригинальный исходный файл:

-Xclang -include-pch -Xclang <PCH_FILE> -Xclang -include -Xclang <PCH_HEADER>

Благодаря опции -Xclang следующий флаг передается напрямую в препроцессор, минуя драйвер, который выполняет поиск соответствующего заголовку .pch/.gch файла. Ссылка на патч: PCH: Clang: Update PCH usage flags to include original header.

Заключение

Наш небольшой патч вошел в CMake 3.17, исправив проблему. Надеюсь, было интересно, и вы узнали чуть больше о работе CMake.

Мы можем рекомендовать использовать эту фичу в реальных проектах.