Хранить все в одной директории - это хорошо для простых проектов, но в большинстве реальных проектов файлы распределены по нескольким директориям. Обычно различные типы файлов или отдельные модули группируются в собственных каталогах, а файлы, относящиеся к логическим функциональным группам, находятся в своей собственной части иерархии каталогов проекта. Хотя структура каталогов может определяться тем, как разработчики представляют себе проект, способ структурирования проекта также влияет на систему сборки.
Две фундаментальные команды CMake в любом сложном проекте - add_subdirectory() и include(). Эти команды добавляют в сборку содержимое из другого файла или каталога, позволяя распределить логику сборки по всей иерархии каталогов, а не заставлять все определять на самом верхнем уровне. Это дает ряд преимуществ:
- Логика сборки локализована, то есть характеристики сборки могут быть определены в том каталоге, где они наиболее актуальны.
- Сборки могут состоять из подкомпонентов, которые определяются независимо от проекта верхнего уровня, использующего их. Это особенно важно, если проект использует такие вещи, как подмодули git, или встраивает иерархию исходных текстов сторонних разработчиков.
- Поскольку каталоги могут быть самодостаточными, становится относительно просто включить или выключить часть сборки, просто выбрав, добавлять или не добавлять каждый каталог.
У add_subdirectory() и include() совершенно разные характеристики, поэтому важно понимать сильные и слабые стороны обеих.
8.1. Команда add_subdirectory()
Команда add_subdirectory() позволяет проекту добавить в сборку другой каталог. Этот каталог должен иметь свой собственный файл CMakeLists.txt, который будет обработан в момент вызова add_subdirectory(). В каталоге сборки проекта будет создан соответствующий подкаталог.
SourceDir не обязательно должен быть подкаталогом относительно исходных текстов, хотя обычно это так. Можно добавить любой каталог, при этом sourceDir может быть указан как абсолютный или относительный путь, последний - относительно текущего каталога исходных текстов. Абсолютные пути обычно нужны только при добавлении каталогов, находящихся вне иерархии дерева исходных текстов.
Обычно binaryDir указывать не нужно. Если это значение опущено, CMake создает в дереве сборки каталог с тем же именем, что и sourceDir. Если sourceDir содержит какие-либо компоненты пути, они будут зеркально отражены в binaryDir, созданном CMake. В качестве альтернативы, binaryDir может быть явно указан как абсолютный или относительный путь, причем последний оценивается относительно текущего каталога бинарных файлов (подробнее об этом будет рассказано ниже). Если sourceDir - это путь вне дерева исходников, CMake требует указания binaryDir, поскольку соответствующий относительный путь уже не может быть построен автоматически.
Необязательное ключевое слово EXCLUDE_FROM_ALL предназначено для управления тем, должны ли цели, определенные в добавляемом подкаталоге, по умолчанию включаться в цель проекта ALL. К сожалению, в некоторых версиях CMake и генераторах проектов оно не всегда работает так, как ожидается, и даже может приводить к нерабочим сборкам. Ключевое слово SYSTEM обычно не используется непосредственно в проектах и рассматривается в разделе 15.7.2, «Пути поиска системных заголовков».
8.1.1. Переменные каталога исходников и каталога сборки
Иногда разработчику необходимо знать расположение каталога сборки, соответствующего текущему каталогу исходного кода, например, при копировании файлов, необходимых во время выполнения программы, или для выполнения пользовательской задачи сборки. С помощью функции add_subdirectory() структура каталогов как дерева исходников, так и дерева сборки может быть произвольно сложной. Даже может быть несколько деревьев сборки, используемых с одним и тем же деревом исходного кода. Поэтому разработчику требуется помощь CMake для определения интересующих его каталогов. Для этого CMake предоставляет ряд переменных, которые отслеживают исходные и бинарные каталоги для обрабатываемого в данный момент файла CMakeLists.txt. Следующие переменные, доступные только для чтения, обновляются автоматически по мере обработки CMake каждого файла. Они всегда содержат абсолютные пути.
- CMAKE_SOURCE_DIR Самый верхний каталог дерева исходных текстов (т.е. где находится самый верхний файл CMakeLists.txt). Эта переменная никогда не меняет своего значения.
- CMAKE_BINARY_DIR Самый верхний каталог дерева сборки. Эта переменная никогда не меняет своего значения.
- CMAKE_CURRENT_SOURCE_DIR Каталог файла CMakeLists.txt, который в данный момент обрабатывается CMake. Он обновляется каждый раз, когда новый файл обрабатывается в результате вызова add_subdirectory(), и восстанавливается обратно, когда обработка этого каталога завершена.
- CMAKE_CURRENT_BINARY_DIR Каталог сборки, соответствующий файлу CMakeLists.txt, который в данный момент обрабатывается CMake. Он изменяется при каждом вызове add_subdirectory() и восстанавливается при возврате из add_subdirectory().
Пример должен помочь продемонстрировать поведение:
CMakeLists.txt верхнего уровня
mysub/CMakeLists.txt
Для приведенного выше примера, если файл CMakeLists.txt верхнего уровня находится в каталоге /somewhere/src, а каталог сборки - /somewhere/build, будет выдан следующий результат:
8.1.2. Область видимости каталога
В разделе 5.4, «Блоки области видимости», обсуждалось понятие области видимости. Одним из эффектов вызова add_subdirectory() является то, что CMake создает новую область видимости для обработки файла CMakeLists.txt этого подкаталога. Эта новая область действует как дочерняя область вызывающей области, подобно тому, как команда block() создает локальную дочернюю область. Эффекты очень похожи:
- Все переменные, определенные в вызывающей области видимости, копируются в дочернюю область видимости подкаталога при входе.
- Любая новая переменная, созданная в дочерней области видимости подкаталога, не будет видна вызывающей области видимости.
- Любое изменение переменной в дочерней области видимости подкаталога является локальным для этой дочерней области видимости.
- Снятие переменной в дочерней области видимости подкаталога не снимает ее в вызывающей области видимости.
CMakeLists.txt
subdir/CMakeLists.txt
Результат:
① myVar определена на родительском уровне.
② childVar не определена на родительском уровне, поэтому он оценивается как пустая строка.
③ myVar все еще виден в дочерней области видимости.
④ childVar остается неопределенным в дочерней области видимости до того, как он будет установлен.
⑤ myVar изменена в дочерней области видимости.
⑥ childVar установлена в дочерней области видимости.
⑦ Когда обработка возвращается в родительскую область видимости, myVar по-прежнему имеет значение, полученное до вызова add_subdirectory(). Модификация myVar в дочерней области видимости не применена к родительской.
⑧ childVar была определена в дочерней области видимости, поэтому она не видна в родительской и оценивается как пустая строка.
Приведенное выше поведение переменных подчеркивает одну из важных характеристик add_subdirectory(). Она позволяет добавляемому каталогу изменять любые переменные по своему усмотрению, не затрагивая переменные в вызывающей области видимости. Это помогает изолировать вызывающую область от потенциально нежелательных изменений.
Как говорилось в разделе 5.4, «Блоки области видимости», ключевое слово PARENT_SCOPE можно использовать с командами set() или unset() для изменения или удаления переменной в родительской области видимости, а не в текущей. Аналогичным образом это работает и для дочерней области видимости, созданной командой add_subdirectory():
CMakeLists.txt
subdir/CMakeLists.txt
Результат:
① Вызов set() не влияет на myVar в дочерней области видимости, поскольку ключевое слово PARENT_SCOPE указывает CMake на изменение родительского myVar, а не локального.
② Родительский myVar был изменен вызовом set() в дочерней области видимости.
Поскольку использование PARENT_SCOPE предотвращает модификацию командой одноименной локальной переменной, можно не вводить в заблуждение, если в локальной области видимости не используется то же имя переменной, что и в родительской. В приведенном выше примере более понятным набором команд будет:
subdir/CMakeLists.txt
Очевидно, что приведенный выше пример является тривиальным, но в реальных проектах может быть много команд, которые вносят свой вклад в создание значения localVar, прежде чем окончательно установить родительскую переменную myVar.
Область видимости влияет не только на переменные, политики и некоторые свойства также имеют схожее с переменными поведение в этом отношении. В случае с политиками каждый вызов add_subdirectory() создает новую область видимости, в которой можно вносить изменения в политику, не затрагивая настройки политики родительского каталога. Аналогично, есть свойства каталога, которые можно установить в файле CMakeLists.txt дочернего каталога, что не повлияет на свойства родительского каталога. Обе эти темы более подробно рассматриваются в соответствующих главах: Глава 12 «Политики» и Глава 9 «Свойства».
8.1.3. Когда вызывать project()
Иногда возникает вопрос, нужно ли вызывать project() в файлах CMakeLists.txt подкаталогов. В большинстве случаев это не обязательно и нежелательно, но допускается. Единственное место, где необходимо вызывать project(), - это самый верхний файл CMakeLists.txt. Прочитав файл CMakeLists.txt верхнего уровня, CMake просматривает содержимое этого файла в поисках вызова функции project(). Если такой вызов не найден, CMake выдаст предупреждение и вставит внутренний вызов project() с включенными по умолчанию языками C и C++. Проекты никогда не должны полагаться на этот механизм, они всегда должны явно вызывать project() самостоятельно. Обратите внимание, что недостаточно вызвать project() через функцию-обертку или через файл, прочитанный через add_subdirectory() или include(), файл верхнего уровня CMakeLists.txt должен вызывать project() напрямую.
Вызов project() в подкаталогах обычно не приносит вреда, но может привести к тому, что CMake придется генерировать дополнительные файлы. По большей части эти дополнительные вызовы project() и генерируемые файлы - просто шум, но в некоторых случаях они могут быть полезны. При использовании генератора проектов Visual Studio каждая команда project() приводит к созданию связанного с ней файла решения. Обычно разработчик загружает файл решения, соответствующий самому верхнему вызову project() (этот файл решения будет находиться в верхней части каталога сборки). Этот файл решения верхнего уровня содержит все цели в проекте. Файлы решений, сгенерированные для любых вызовов project() в подкаталогах, будут содержать более урезанный вид, содержащий только цели из этого каталога и ниже, а также любые другие цели из остальных частей сборки, от которых они зависят. Разработчики могут загружать эти подрешения вместо одного верхнего уровня для получения более урезанного представления проекта, позволяющего им сосредоточиться на меньшем подмножестве набора целей. Для очень больших проектов с большим количеством целей это может быть особенно полезно.
Генератор Xcode ведет себя аналогичным образом, создавая проект Xcode для каждого вызова project(). Эти проекты Xcode могут быть загружены для аналогичного урезанного представления, но, в отличие от генераторов Visual Studio, они не включают логику для сборки целей за пределами области видимости каталога или ниже. Разработчик отвечает за то, чтобы все, что требуется за пределами этого урезанного представления, уже было собрано. На практике это означает, что проект верхнего уровня, скорее всего, должен быть загружен и собран первым, прежде чем переходить к урезанному проекту Xcode.
8.2. Команда include()
Другой метод, который CMake предоставляет для получения содержимого из других каталогов, - это команда include(), которая имеет следующие две формы:
Первая форма в некоторой степени аналогична add_subdirectory(), но есть важные отличия:
- include() ожидает имя файла для чтения, в то время как add_subdirectory() ожидает каталог и будет искать файл CMakeLists.txt в этом каталоге. Имя файла, передаваемого в include(), обычно имеет расширение .cmake, но может быть любым.
- include() не создает новую область видимости, в то время как add_subdirectory() создает.
- Обе команды по умолчанию вводят новую область действия политики, но команде include() можно указать не делать этого с помощью опции NO_POLICY_SCOPE (у add_subdirectory() такой опции нет). Дополнительные сведения об обработке области действия политики см. в главе 12 «Политики».
- Значение переменных CMAKE_CURRENT_SOURCE_DIR и CMAKE_CURRENT_BINARY_DIR не изменяется при обработке файла, вызванного include(), в то время как для add_subdirectory() они изменяются. Подробнее об этом будет рассказано ниже.
Вторая форма команды include() служит совершенно другой цели. Она используется для загрузки именованного модуля, что подробно рассматривается в главе 11 «Модули». Все пункты, кроме первого, справедливы и для этой второй формы.
Поскольку значение CMAKE_CURRENT_SOURCE_DIR не изменяется при вызове include(), может показаться, что для включаемого файла трудно определить каталог, в котором он находится. CMAKE_CURRENT_SOURCE_DIR будет содержать расположение файла, из которого была вызвана include(). Кроме того, в отличие от add_subdirectory(), где fileName всегда будет CMakeLists.txt, при использовании include() имя файла может быть любым, поэтому включаемому файлу может быть сложно определить свое имя. Для решения подобных ситуаций CMake предоставляет дополнительный набор переменных:
- CMAKE_CURRENT_LIST_DIR Аналогична CMAKE_CURRENT_SOURCE_DIR, за исключением того, что она будет обновляться при обработке включенного файла. Это переменная, которую следует использовать, когда требуется каталог текущего обрабатываемого файла, независимо от того, как он был добавлен в сборку. Она всегда будет содержать абсолютный путь.
- CMAKE_CURRENT_LIST_FILE Всегда указывает имя файла, который обрабатывается в данный момент. Он всегда содержит абсолютный путь к файлу, а не просто имя файла.
- CMAKE_CURRENT_LIST_LINE Содержит номер строки обрабатываемого в данный момент файла. Эта переменная нужна редко, но может оказаться полезной в некоторых сценариях отладки.
Обратите внимание, что три вышеупомянутые переменные работают для любого файла, обрабатываемого CMake, а не только для тех, которые были вызваны командой include(). Они имеют те же значения, что описаны выше, даже для файла CMakeLists.txt, притягиваемого командой add_subdirectory(), и в этом случае CMAKE_CURRENT_LIST_DIR будет иметь то же значение, что и CMAKE_CURRENT_SOURCE_DIR. Следующий пример демонстрирует такое поведение:
CMakeLists.txt
subdir/CMakeLists.txt
Результат:
Приведенный выше пример также подчеркивает еще одну интересную особенность команды include(). Она может быть использована для включения содержимого файла, который уже был включен в сборку ранее. Если разные подкаталоги большого сложного проекта захотят использовать код CMake из некоторого файла в общей части проекта, они могут включить этот файл независимо друг от друга.
8.3. Переменные, связанные с командой project()
Как будет показано в последующих главах, в различных сценариях требуются пути относительно местоположения каталога исходного кода или каталога сборки. Рассмотрим один из таких примеров, когда проекту нужен путь к файлу, находящемуся в каталоге исходных текстов. Исходя из раздела 7.1.1, «Переменные каталога исходников и каталога сборки», CMAKE_SOURCE_DIR кажется естественным вариантом, позволяющим использовать путь типа ${CMAKE_SOURCE_DIR}/someFile. Но подумайте, что произойдет, если этот проект позже будет включен в другой родительский проект путем добавления его в родительскую сборку через add_subdirectory(). Его можно будет использовать как git-подмодуль или извлекать по требованию, используя техники, подобные тем, что обсуждаются в главе 30, «FetchContent». То, что раньше было вершиной дерева исходных текстов проекта, теперь является подкаталогом в дереве исходных текстов родительского проекта. CMAKE_SOURCE_DIR теперь указывает на вершину родительского проекта, поэтому путь к файлу будет указывать на неправильный каталог. Аналогичная ловушка существует и для CMAKE_BINARY_DIR.
Описанный выше сценарий удивительно часто встречается в онлайн-учебниках и старых проектах, но его можно легко избежать. Команда project() устанавливает некоторые переменные, которые обеспечивают гораздо более надежный способ определения путей относительно мест в иерархии каталогов. Следующие переменные будут доступны после того, как команда project() будет вызвана хотя бы один раз:
- PROJECT_SOURCE_DIR Каталог исходных текстов последнего вызова команды project() в текущей или родительской области видимости. Имя проекта (т.е. первый аргумент команды project()) не имеет значения.
- PROJECT_BINARY_DIR Каталог сборки, соответствующий каталогу исходных текстов, определенному PROJECT_SOURCE_DIR.
- projectName_SOURCE_DIR Каталог исходных текстов последнего вызова project(projectName) в текущей области видимости или любой родительской области видимости. Это привязано к конкретному имени проекта и, следовательно, к конкретному вызову project().
- projectName_BINARY_DIR Каталог сборки, соответствующий каталогу, определенному projectName_SOURCE_DIR.
Следующий пример демонстрирует, как можно использовать эти переменные (переменные ..._BINARY_DIR следуют аналогично переменным ..._SOURCE_DIR).
CMakeLists.txt
child/CMakeLists.txt
child/grandchild/CMakeLists.txt
Запуск cmake на верхнем уровне иерархии проектов даст результат, подобный следующему:
Приведенный выше пример демонстрирует универсальность переменных, связанных с проектом. Их можно использовать из любой части иерархии каталогов для надежной ссылки на любой другой каталог в проекте. Для сценария, рассмотренного в начале этого раздела, использование ${PROJECT_SOURCE_DIR}/someFile или, возможно, ${projectName_SOURCE_DIR}/someFile вместо ${CMAKE_SOURCE_DIR}/someFile гарантирует, что путь к someFile будет правильным, независимо от того, собирается ли проект отдельно или включается в большую иерархию проектов.
Некоторые иерархические схемы сборки позволяют собирать проект либо отдельно, либо как часть более крупного родительского проекта (см. главу 30, «FetchContent»). Некоторые части проекта (например, настройка поддержки упаковки) могут иметь смысл только в том случае, если он является верхним уровнем сборки. Проект может определить, является ли он верхним уровнем, сравнив значение CMAKE_SOURCE_DIR с CMAKE_CURRENT_SOURCE_DIR.
Приведенная выше техника поддерживается всеми версиями CMake и является очень распространенным шаблоном. В CMake 3.21 и более поздних версиях появилась специальная переменная PROJECT_IS_TOP_LEVEL, которая позволяет достичь того же результата, но более четко выражает его:
Значение PROJECT_IS_TOP_LEVEL будет истинным, если последний вызов project() был сделан из файла CMakeLists.txt верхнего уровня. Аналогичная переменная, <projectName>_IS_TOP_LEVEL, также определяется в CMake 3.21 или более поздней версии для каждого вызова project(). Она создается как кэш-переменная, поэтому может быть прочитана из любой области видимости. <projectName> соответствует имени, заданному в команде project(). Эта альтернативная переменная полезна, когда между текущей областью видимости и областью видимости интересующего проекта могут быть промежуточные вызовы project().
8.4. Досрочное завершение
Бывают случаи, когда проект хочет прекратить обработку оставшейся части текущего файла и вернуть управление вызывающей стороне. Для этого можно использовать команду return(). Если return() не вызывается изнутри функции, она завершает обработку текущего файла, независимо от того, был ли он добавлен с помощью include() или add_subdirectory(). Последствия вызова return() внутри функции рассматриваются в разделе 8.4, «Возврат значений», включая распространенные ошибки, которые могут привести к непреднамеренному возврату из текущего файла.
В CMake 3.24 и более ранних версиях команда return() не может возвращать вызывающей стороне никаких значений. Начиная с CMake 3.25, команда return() принимает ключевое слово PROPAGATE, которое имеет сходство с аналогичным ключевым словом команды block(). Переменные, указанные после ключевого слова PROPAGATE, будут обновлены в той области видимости, в которую возвращается управление. Исторически сложилось так, что команда return() игнорирует все переданные ей аргументы. Поэтому при использовании ключевого слова PROPAGATE политика CMP0140 должна быть установлена в значение NEW, чтобы указать, что старое поведение неприменимо (см. главу 12 «Политики»).
CMakeLists.txt
subdir/CMakeLists.txt
Стоит выделить два случая, связанных с распространением переменных и взаимодействием с командой block(). Оба случая являются следствием того, что команда return() обновляет переменные в области видимости, в которую она возвращается. В первом случае, если возвращаемая область видимости находится внутри блока, то обновляется область видимости только этого блока.
CMakeLists.txt
Другой случай, на котором стоит остановиться, более интересен. Если оператор return() находится внутри блока, то этот блок не влияет на распространение переменных в возвращаемую область видимости.
CMakeLists.txt
subdir/CMakeLists.txt
Следует сказать несколько слов предостережения по поводу использования return(PROPAGATE) с областями видимости каталогов. Хотя это может показаться привлекательным способом передачи информации обратно в родительскую область видимости, такое использование не соответствует подходу, где логика опирается на объявление цели, принятому в CMake. Передача переменных в родительские области видимости приводит к тому, что структура проекта становится более похожей на старые методы, основанные на переменных. Передача переменных с помощью return() более уместна при возврате из функций, как обсуждается в разделе 8.4, «Возврат значений».
Команда return() - не единственный способ досрочно завершить обработку файла. Как отмечалось в предыдущем разделе, разные части проекта могут включать один и тот же файл из нескольких мест. Иногда бывает желательно проверить это и включить файл только один раз, возвращаясь досрочно при последующих включениях, чтобы не обрабатывать файл несколько раз. Это очень похоже на ситуацию с заголовками C и C++. Поэтому часто можно встретить подобную форму защиты от включения:
В CMake 3.10 или более поздней версии это можно выразить более кратко и надежно с помощью специальной команды, поведение которой аналогично #pragma once из C и C++:
По сравнению с ручным написанием кода if-endif этот способ более надежен, поскольку он обрабатывает имя защитной переменной внутри. Команда также принимает необязательный аргумент DIRECTORY или GLOBAL, чтобы указать другую область видимости, в которой нужно проверить, не обрабатывался ли файл ранее. Однако эти ключевые слова вряд ли понадобятся в большинстве ситуаций. Если ни один из аргументов не указан, предполагается текущая область видимости, и эффект будет эквивалентен приведенному выше коду if-endif. GLOBAL гарантирует, что команда завершит обработку файла, если он был обработан до этого где-либо еще в проекте. DIRECTORY проверяет наличие предыдущей обработки только в пределах области видимости текущего каталога и ниже по иерархии.
8.5. Рекомендуемые практики
Выбор между использованием add_subdirectory() или include() для включения другого каталога в сборку не всегда очевиден. С одной стороны, add_subdirectory() проще и лучше справляется с сохранением относительной автономности каталогов, поскольку создает собственную область видимости. С другой стороны, некоторые команды CMake имеют ограничения, которые позволяют им работать только с вещами, определенными в текущей области видимости файла, поэтому include() работает лучше в таких случаях. В разделе 15.2.6, «Исходные файлы» и разделе 34.5.1, «Построение цели по каталогам», рассматриваются аспекты этой темы.
Как правило, в большинстве простых проектов лучше использовать add_subdirectory(), а не include(). Это способствует более чистому определению проекта и позволяет в CMakeLists.txt для данной директории сосредоточиться только на том, что эта директория должна определять. Следование этой стратегии будет способствовать лучшему расположению информации во всем проекте, а также будет иметь тенденцию вводить сложности только там, где это необходимо и где это приносит пользу. Не то чтобы include() сама по себе была сложнее add_subdirectory(), но использование include() приводит к тому, что пути к файлам должны быть прописаны более явно, поскольку CMake считает текущим исходным каталогом не тот каталог, в который включен файл. Многие ограничения, связанные с вызовом определенных команд из разных каталогов, также были сняты в более поздних версиях CMake, что еще больше усиливает аргумент в пользу add_subdirectory().
Независимо от того, используется ли add_subdirectory(), include() или их комбинация, переменная CMAKE_CURRENT_LIST_DIR, как правило, будет более удачным выбором, чем CMAKE_CURRENT_SOURCE_DIR. Если выработать привычку использовать CMAKE_CURRENT_LIST_DIR на ранних этапах, то по мере роста сложности проекта будет гораздо проще переключаться между add_subdirectory() и include(), а также перемещать целые каталоги для реструктуризации проекта.
По возможности избегайте использования переменных CMAKE_SOURCE_DIR и CMAKE_BINARY_DIR, поскольку они обычно нарушают способность проекта быть включенным в иерархию более крупных проектов. В подавляющем большинстве случаев целесообразнее использовать переменные PROJECT_SOURCE_DIR и PROJECT_BINARY_DIR или их эквиваленты для конкретного проекта projectName_SOURCE_DIR и projectName_BINARY_DIR.
Избегайте использования ключевого слова PROPAGATE с операторами return(), которые завершают обработку текущего файла. Передача переменных в родительский файл нарушает наилучшую практику, согласно которой лучше прикреплять информацию к целям, а не передавать ее в переменных. В главе 15, «Инструментарий компилятора и компоновщика», рассматривается множество материалов, связанных с предпочтением практики, ориентированной на цели.
Если для проекта требуется CMake 3.10 или более поздняя версия, предпочтите использовать команду include_guard() без аргументов вместо явного блока if-endif в случаях, когда необходимо предотвратить многократное включение файла.
Избегайте практики произвольного вызова команды project() в файле CMakeLists.txt каждого подкаталога. Помещайте команду project() в файл CMakeLists.txt подкаталога только в том случае, если этот подкаталог можно рассматривать как более или менее самостоятельный проект. Если только вся сборка не имеет очень большого количества целей, нет особой необходимости вызывать project() где-либо, кроме как в файле CMakeLists.txt верхнего уровня.
Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott