Найти в Дзене
coding style

CMake - Практическое руководство. Глава 13. Политики (Крэйг Скотт, перевод на русский язык)

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

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

Контроль за использованием старого или нового поведения осуществляется с помощью механизмов политик CMake. В целом, с политиками разработчики сталкиваются не так уж часто, в основном только когда CMake выдает предупреждение о том, что проект полагается на поведение старой версии. Когда разработчики переходят на более свежий релиз CMake, новая версия CMake иногда выдает такие предупреждения, чтобы подчеркнуть, как проект должен быть обновлен для использования нового поведения.

13.1. Контроль политики

Функционал политик CMake тесно связан с командой cmake_minimum_required(), которая была представлена еще в главе 3, "Минимальный проект". Эта команда не только указывает минимальную версию CMake, которая требуется проекту, но и устанавливает поведение CMake в соответствии с указанной версией. Таким образом, когда проект начинается с cmake_minimum_required(VERSION 3.2), это говорит о том, что требуется как минимум CMake 3.2, а также о том, что проект ожидает, что CMake будет вести себя как релиз 3.2. Это дает проектам уверенность в том, что разработчики смогут обновить CMake до любой более новой версии в удобное для них время, и проект будет собираться так же, как и раньше.

Однако иногда в проекте может потребоваться более тонкий контроль, чем тот, который обеспечивает команда cmake_minimum_required(). Рассмотрим следующие сценарии:

  • Проект хочет установить низкую минимальную версию CMake, но при этом использовать преимущества более новых версий, если они доступны.
  • Часть проекта не может быть изменена (например, она может быть взята из внешнего репозитория кода, доступного только для чтения), и она полагается на старое поведение, которое было изменено в более новых версиях CMake. Однако остальная часть проекта хочет перейти на новое поведение.
  • Проект сильно зависит от старого поведения, обновление которого потребует нетривиального объема работы. Некоторые части проекта хотят использовать новые возможности CMake, но старое поведение для этого конкретного изменения должно быть сохранено до тех пор, пока не будет выделено время для обновления проекта.

Это несколько распространенных примеров, когда одного лишь контроля высокого уровня, обеспечиваемого командой cmake_minimum_required(), недостаточно. Более конкретный контроль над политиками осуществляется с помощью команды cmake_policy(), которая имеет несколько форм, действующих на разных уровнях детализации. Форма, действующая на самом грубом уровне, является близким родственником cmake_minimum_required():

-2

В этой форме команда изменяет поведение CMake в соответствии с указанной версией. Команда cmake_minimum_required() неявно вызывает эту форму для установки поведения CMake. Эти две команды в значительной степени взаимозаменяемы, за исключением верхней части проекта, где вызов cmake_minimum_required() является обязательным. За исключением начала файла CMakeLists.txt верхнего уровня, использование cmake_policy() обычно более четко передает намерения, когда проекту нужно обеспечить поведение определенной версии для части проекта, как показано в следующем примере:

-3

CMake 3.12 расширяет эту возможность, позволяя указывать в cmake_minimum_required() или cmake_policy(VERSION) диапазон версий, а не одну версию. Диапазон указывается с помощью трех точек ... между минимальной и максимальной версиями без пробелов. Диапазон указывает на то, что используемая версия CMake должна быть не ниже минимальной, а поведение должно соответствовать наименьшему из двух: указанной максимальной версии CMake и запущенной версии CMake. Это позволяет проекту эффективно сказать: «Мне нужен как минимум CMake X, но я могу безопасно работать с версиями до CMake Y». В следующем примере показаны два способа, как проект может требовать CMake 3.7, но при этом поддерживать более новое поведение для всех политик вплоть до CMake 3.12, если запущенная версия CMake поддерживает их:

-4

Версии CMake до 3.12 будут видеть только один номер версии и игнорировать часть ...3.12, тогда как 3.12 и более поздние версии будут понимать, что это означает диапазон.

CMake также предоставляет возможность контролировать изменение поведения по частям с помощью формы SET:

-5

Каждому индивидуальному изменению поведения присваивается свой номер политики в виде CMPxxxx, где xxxx - всегда четыре цифры. Указывая NEW или OLD, проект указывает CMake на использование нового или старого поведения для данной политики. В документации CMake приведен полный список политик, а также объяснение поведения OLD и NEW для каждой из них.

Например, до версии 3.0 CMake позволял проекту вызывать get_target_property() с именем несуществующей цели. В таком случае значение свойства возвращалось в виде -NOTFOUND, а не вызывало ошибку, но, по всей вероятности, проект содержал некорректную логику. Поэтому, начиная с версии 3.0, CMake при возникновении подобной ситуации завершает работу с ошибкой.

Если проект полагался на старое поведение, он мог продолжать делать это, используя политику CMP0045 следующим образом:

-6

Необходимость в установке политики NEW встречается реже. Одна из ситуаций - когда проект хочет установить низкую минимальную версию CMake, но при этом воспользоваться преимуществами более поздних функций, если используется более поздняя версия. Например, в CMake 3.2 была введена политика CMP0055, обеспечивающая строгую проверку использования команды break(). Если проект все еще хотел поддерживать сборку с более ранними версиями CMake, то дополнительные проверки должны были быть явно включены при запуске с более поздними версиями CMake.

-7

Проверка переменной CMAKE_VERSION - это один из способов определить, доступна ли политика, но команда if() предоставляет более прямой способ, используя форму if(POLICY...). Вышеприведенное можно реализовать следующим образом:

-8

Также можно получить текущее состояние конкретной политики. Основная ситуация, в которой может потребоваться чтение текущих настроек политики - это файл модуля который может быть предоставлен самим CMake или проектом. Однако было бы необычно, если бы модуль проекта изменил свое поведение на основе настроек политики.

-9

Значение, хранящееся в outVar, будет OLD, NEW или пустой строкой. Команды cmake_minimum_required(VERSION...) и cmake_policy(VERSION...) сбрасывают состояние всех политик. Те политики, которые были введены в указанной версии CMake или ранее, сбрасываются в состояние NEW. Те политики, которые были добавлены после указанной версии, будут фактически сброшены в пустое состояние.

Если CMake обнаруживает, что проект делает что-то опирающееся на старое поведение, либо конфликтует с новым поведением, он может предупредить, что соответствующая политика не установлена. Эти предупреждения - наиболее распространенный способ ознакомления разработчиков с функциональностью CMake в области политик. Они предназначены для того, чтобы информировать и побуждать разработчиков обновить проект в соответствии с новым поведением. В некоторых случаях предупреждение об устаревании может быть выдано, даже если политика была явно установлена, но это, как правило, относится только к политике, которая уже давно (много релизов) документирована как устаревшая.

Иногда предупреждения политики не могут быть сразу локализованы, и в тоже время являются нежелательными. Предпочтительным способом решения этой проблемы является явная установка политики на желаемое поведение (OLD или NEW), что отменяет предупреждение. Однако это не всегда возможно, например, когда более глубокая часть проекта делает свой собственный вызов cmake_minimum_required(VERSION...) или cmake_policy(VERSION...), тем самым сбрасывая состояния политики. В качестве временного способа обхода таких ситуаций CMake предоставляет переменные CMAKE_POLICY_DEFAULT_CMPxxxx и CMAKE_POLICY_WARNING_CMPxxxx, где xxxx - обычный четырехзначный номер политики. Они не предназначены для установки проектом, а скорее разработчиком в качестве временной переменной кэша для включения/выключения предупреждения или проверки того, выдает ли проект предупреждения с включенной определенной политикой. В конечном счете, долгосрочным решением является устранение основной проблемы, на которую указывает предупреждение. Тем не менее, иногда в проекте может быть целесообразно установить одну из этих переменных, чтобы убрать предупреждение, которое как известно не является критическим.

13.2. Область видимости политики

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

Для сохранения и восстановления из стека политик доступны два метода:

-10
-11

Из двух методов первый, использующий block(), является более надежным. Независимо от того, как поток управления покидает блок, политики восстанавливаются до состояния, в котором они находились до входа в блок. Это означает, что такие команды, как return(), break() и continue(), могут свободно использоваться внутри блока.

Второй метод, использующий cmake_policy(), более хрупок, поскольку он зависит от того, что проект гарантирует, что вызовет cmake_policy(POP) ровно один раз для каждого cmake_policy(PUSH). Это может быть непросто для больших проектов. Частым источником ошибок является вызов cmake_policy(PUSH) в начале длинного файла и cmake_policy(POP) в конце, но возврат раньше времени где-то в середине файла без вызова cmake_policy(POP). Это часто происходит, когда в файле изначально не было оператора return() на момент добавления политики с помощью push-pop, но return() был добавлен позже. Файлы модулей - одно из наиболее распространенных мест, где стеком политик можно манипулировать подобным образом и где часто возникают ошибки такого рода.

-12

Замена вышеприведенной функции на block() позволяет избежать проблемы:

-13

Некоторые команды неявно заносят новое состояние политики в стек и снова восстанавливают его прежде чем вернуться к вызывающей стороне. Важными примерами этого являются команды add_subdirectory(), include() и find_package(). Команды include() и find_package() также поддерживают опцию NO_POLICY_SCOPE, которая предотвращает автоматическое сохранение/восстановление стека политик (у add_subdirectory() такой опции нет). В очень ранних версиях CMake команды include() и find_package() автоматически не делали этого. Опция NO_POLICY_SCOPE была добавлена как способ для проектов, использующих более поздние версии CMake, чтобы вернуться к старому поведению для определенных частей проекта, но ее использование не рекомендуется и не должно быть необходимым для новых проектов.

13.3. Рекомендуемые практики

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

При выборе способа указания версии CMake для соответствия определенному поведению, выбор между cmake_minimum_required(VERSION) и cmake_policy(VERSION) обычно приходится на последнюю. Два основных исключения из этого правила - в начале файла CMakeLists.txt верхнего уровня проекта и в начале файла модуля, который может быть использован в других проектах. В последнем случае предпочтительнее использовать cmake_minimum_required(VERSION), поскольку проекты, использующие модуль, могут устанавливать свои собственные минимальные версии CMake, а модуль - свои требования к минимальной версии. Кроме этих случаев, cmake_policy(VERSION) обычно выражает намерение более четко, но с точки зрения определения политик обе команды фактически достигают одного и того же.

В тех случаях, когда проекту необходимо манипулировать определенной политикой, ему следует проверять наличие этой политики с помощью if(POLICY...), а не проверять переменную CMAKE_VERSION. Это приводит к большей согласованности кода. Сравните следующие два способа задания поведения политики и обратите внимание на то, как проверка и применение используют последовательный подход:

-14

Если проекту необходимо локально управлять несколькими отдельными политиками, окружите этот раздел вызовами block(SCOPE_FOR POLICIES) и endblock(). Если необходимо поддерживать CMake 3.24 или более раннюю версию, используйте cmake_policy(PUSH) и cmake_policy(POP). Окружение кода любой парой команд гарантирует, что остальная часть области видимости будет изолирована от изменений. Если вы используете cmake_policy() для определения этих областей, обратите особое внимание на возможные операторы return(), break() или continue(), которые выходят из этого участка кода, и убедитесь, что ни один push не остался без соответствующего pop.

Обратите внимание, что add_subdirectory(), include() и find_package() делают автоматическое сохранение/восстановление стека политик. Для изоляции изменений их политики от вызывающей области видимости не требуется явный блок или push-pop. Проектам следует избегать ключевого слова NO_POLICY_SCOPE в этих командах, поскольку оно предназначено только для устранения изменений в поведении очень ранних версий CMake. NO_POLICY_SCOPE редко подходит для новых проектов.

Старайтесь не изменять параметры политики внутри функции, если только не используете конструкции block(SCOPE_FOR POLICIES) и endblock() или cmake_policy(PUSH) и cmake_policy(POP) в теле функции. Поскольку функции не вводят новую область видимости для политики, изменение политики может повлиять на вызывающую сторону, если только не будет должным образом изолировано с помощью соответствующей логики. Кроме того, настройки политики для функции берутся из области видимости, в которой функция была определена, а не из той, из которой она вызывается. Поэтому предпочтите корректировать любые настройки политики в области видимости содержащей определение функции, а не в самой функции.

В крайнем случае, переменные CMAKE_POLICY_DEFAULT_CMPxxxx и CMAKE_POLICY_WARNING_CMPxxxx могут позволить разработчику или проекту обойти некоторые специфические ситуации, связанные с политикой. Разработчики могут использовать их для временного изменения значения по умолчанию для определенного параметра политики или для предотвращения предупреждений о конкретной политике. Как правило, проекты не должны устанавливать эти переменные так, чтобы разработчики могли контролировать их локально. Тем не менее, в некоторых ситуациях они могут быть использованы для того, чтобы поведение или предупреждение о конкретной политике сохранялось даже после вызовов cmake_minimum_required() или cmake_policy(VERSION). По возможности, проекты должны быть обновлены до более нового поведения, а не полагаться на эти переменные.

Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott