В этой простенькой статье расскажу о проблемах, с которыми я столкнулся при изучении CMake, и о их решениях. Если кратко, то с помощью .cmake файла от TheLartians, а также замечания от Sazonov у меня получилось из такого:

Сделать такое:

Для кого и зачем эта статья?
В первую очередь данная статья ориентирована на новичков в CMake, которые как и я изучают его по документациям и туториалам в интернете. К сожалению, в ру сегменте мне не удалось найти точную и конкретную информацию по настройке структуры проекта (Возможно я плохо искал, буду рад увидеть в комментариях ссылки на источники). Потому пришлось обратится к англоязычным ресурсам и статьям, которые понять мне(плохо знающему данный язык) было сложновато. Пишу я это статью с желанием помочь таким же как и я, дабы люди могли больше уделять внимание именно разработке, а не поиску решения , с виду, простых проблем.
Подмечу что мой пример и мои проекты хранят заголовочные файлы вместе с CPP файлами, то есть у меня нету в проектах директории Include, где обычно располагают хейдеры. Для моих пока что мелких проектов создавать библиотеки не имеет смысла. Имейте это ввиду!
Приступим к решению проблемы
UPD: Как и ожидалось, моё решение проблемы оказалось в значительной степени нагромождено лишним и ненужным .В комментариях к статье пользователь Sazonov указал, что создать древовидную структуру можно проще, если воспользоваться второй сигнатурой команды source_group, которая выглядит следующим образом:
source_group(TREE <root> [PREFIX <prefix>] [FILES <src>...])
В качестве аргументов команда принимает абсолютный путь к папке проекта <root>), опциональный параметр <prefix>, добавляющий к началу древа проекта директорию, которую мы укажем (она не обязательно должна существовать, так как этот пункт влияет только на вид внутри Visual Studio), и опциональный параметр с путями файлов, которые следует обработать. Разберём сначала решение проблемы с помощью данной команды. Допустим имеется следующая структура проекта :
ExampleFolderStructure . ├── CMakeLists.txt └── src ├── Game │ ├── Game.cpp | ├── Game.h | └── GameObjects │ ├── IGameObject.cpp | └── IGameObject.h └── ResourceManager ├── ResourceManager.cpp └── ResourceManager.h
Для создания древа файлов понадобится вписать в главный CMakelists.txt следующее:
cmake_minimum_required(VERSION 3.8) project(my_game) FILE(GLOB_RECURSE headers "src/*.h") FILE(GLOB_RECURSE sources "src/*.cpp") add_executable(my_game ${sources} ${headers}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${sources} ${headers})
Абсолютный путь к папке проекта можно либо записать вручную(для меня это "E:/ExampleFolderStructure"), либо воспользоваться кэшированной переменной ${CMAKE_CURRENT_SOURCE_DIR}.
На этом можно было бы и закончить статью, однако кому интересна данная тема, могут обратится к оставшейся части, в которой более подробно объяснены все детали, но уже другой реализацией, которая всё также работает. В ней вы сможете также найти команду для обёртывания таргетов, таких как ALL_BUILD и ZERO_CHECK, в папки.
Подробная инструкция с другой реализацией
Cначала создадим таргет со всеми исполняемыми и заголовочными файлами. Сделать это можно либо введя все файлы вручную, либо воспользовавшись командой file. Воспользуемся вторым подходом:
cmake_minimum_required(VERSION 3.8) project(my_game) set_property(GLOBAL PROPERTY USE_FOLDERS ON) FILE(GLOB_RECURSE headers "src/*.h") FILE(GLOB_RECURSE sources "src/*.cpp") add_executable(my_game ${headers} ${sources})
Первые две строчки думаю понятны. Пятая строка устанавливает свойство USE_FOLDER в true, и теперь мы сможем отправить таргеты внешних библиотек(Например, glad или glfw если вы работаете с OpenGL) в папки, чтобы они меньше мешались. 7 и 8 строка создает переменные headers и sources в которых находятся пути ко всем файлам каждого типа. Если рассмотреть поподробнее, то первый аргумент указывает на действие, которое будет выполнять команда file(в данном случае поиск путей к файлам определенного типа), второй параметр за переменную, куда всё будет записано, а третий аргумент отвечает за расположение директории, в которой нужно искать файлы определённого расширения. Уже теперь наш проект выглядит куда симпатичнее

ALL_BUILD и ZERO_CHECK были автоматический добавлены в папку по умолчанию, но Header Files и Source Files осталисьЗатем опционально можно обернуть наш таргет my_game в папку с помощью set_target_properties():
set_target_properties(my_game PROPERTIES FOLDER "myGameFolder")
Первый аргумент отвечает за таргет, который будет помещён в папку с названием, который задаётся 4 аргументом.
Теперь нам нужно будет воспользоваться функцией от пользователя TheLartians с его репозитория GroupSourcesByFolder.cmake. Можно как в инструкции импортировать его cmake файл в свой проект, но для простоты просто скопируем саму функцию, которая выглядит следующим образом:
function(GroupSourcesByFolder target) set(SOURCE_GROUP_DELIMITER "/") set(last_dir "") set(files "") get_target_property(sources ${target} SOURCES) foreach(file ${sources}) file(RELATIVE_PATH relative_file "${PROJECT_SOURCE_DIR}" ${file}) get_filename_component(dir "${relative_file}" PATH) if(NOT "${dir}" STREQUAL "${last_dir}") if(files) source_group("${last_dir}" FILES ${files}) endif() set(files "") endif() set(files ${files} ${file}) set(last_dir "${dir}") endforeach() if(files) source_group("${last_dir}" FILES ${files}) endif() endfunction()
Может показаться страшным, но если просто вчитаться, что именно написано и какие команды используются, то станет более менее понятно. Функция GroupSourcesByFolder принимает в качестве аргумента таргет, из которого вытягивает пути к используемым файлам, которые до этого мы сами передали ему:
get_target_property(sources ${target} SOURCES) #в sources теперь записаны полные пути к файлам
Затем запускается цикл, в каждом проходе которого мы получаем относительную папку файла:
file(RELATIVE_PATH relative_file "${PROJECT_SOURCE_DIR}" ${file}) get_filename_component(dir "${relative_file}" PATH) # теперь dir хранит в себе относительный путь к директории, где находится текущй путь файла
Далее происходит интересная проверка. Если директория текущего файла совпадает с директории предыдущего файла(мы знаем это, потому что после каждого прохода цикла сохраняем dir в last_dir), то мы только добавляем путь текущего файла к переменной files. Если же случилось так, что при прохождении по циклу текущий файл будет из другой директории, то мы все накопленные пути в переменной files добавляем в last_dir c помощью следующей команды:
source_group("${last_dir}" FILES ${files})
В конце функции, после цикла, имеется ещё одна проверка, чтобы последние файлы добавились в нужную директорию, так как цикл просматривает последние файлы, но не добавляет их. Теперь осталось только вызывать функцию и всё! Весь CMakelists.txt выглядит следующим образом:
cmake_minimum_required(VERSION 3.8) project(my_game) set_property(GLOBAL PROPERTY USE_FOLDERS ON) FILE(GLOB_RECURSE headers "src/*.h") FILE(GLOB_RECURSE sources "src/*.cpp") add_executable(my_game ${headers} ${sources}) set_target_properties(my_game PROPERTIES FOLDER "myGameFolder") function(GroupSourcesByFolder target) set(SOURCE_GROUP_DELIMITER "/") set(last_dir "") set(files "") get_target_property(sources ${target} SOURCES) foreach(file ${sources}) file(RELATIVE_PATH relative_file "${PROJECT_SOURCE_DIR}" ${file}) get_filename_component(dir "${relative_file}" PATH) if(NOT "${dir}" STREQUAL "${last_dir}") if(files) source_group("${last_dir}" FILES ${files}) message(${files}) endif() set(files "") endif() set(files ${files} ${file}) set(last_dir "${dir}") endforeach() if(files) source_group("${last_dir}" FILES ${files}) endif() endfunction() GROUPSOURCESBYFOLDER(my_game)
Теперь если запустить конфигурацию и билд через GUI CMake-a, мы получим вот такой вот результат:

Рекомендую подебагерить и поизучать код самостоятельно с помощью команды message.
Хочу подметить, что так как мы используем команду file, то при изменении структуры проекта или внесения дополнительных файлов понадобится заново собирать проект через CMake, а не Visual Studio. Не получится в самой визуалке добавить в таргет исполняемых фалов новый и сразу же вызвать билд ctrl + shift + B. Нужно будет либо через командную строку вызвать создание проекта, либо через графическую оболочку CMake. Если вас такой подход не устраивает, то следует самостоятельно прописывать все используемые файлы, как в примере ниже:
cmake_minimum_required(VERSION 2.8.10) project(Main CXX) set( source_list "main.cpp" "Logging/MyLog.cpp" "Logging/MyLog.h" "InputOutput/InputOutput.cpp" "InputOutput/InputOutput.h" ) add_executable(Main ${source_list}) foreach(source IN LISTS source_list) get_filename_component(source_path "${source}" PATH) string(REPLACE "/" "\\" source_path_msvc "${source_path}") source_group("${source_path_msvc}" FILES "${source}") endforeach()
Пока писал статью, нашел на русском stack overflow вопрос в котором пользователь Евгений привел более упрощенную версию цикла foreach(код выше и есть его). Данный код работает аналогично, только имеет некоторые мелкие изменения, которые позволяют работать только в Visual Studio не обращаясь за билдингом к CMake-GUI или CMD.
Итоги
Теперь нам удалось создать древовидную структуру хранения файлов, как и в самом проводнике, что значительно повышает читабельность проекта и ускоряет навигацию по нему. До того, как я прибегнул к такому решению, мой проект выглядел следующим образом:

А уже после вот так:

Начиная изучать CMake и OpenGL как то и не задумывался о виде самого проекта и просто писал код, но по мере увеличения количество файлов, поиск нужного стало приносить боль.
Возможно для кого то может показаться странным и даже глупым то, что я пишу здесь. И может быть вы и будете правы. На изучение CMake я отвожу меньше времени чем OpenGL и потому у меня возникают порой глупые вопросы и ошибки, решение которых, однако, порой бывает трудно найти. Если вы нашли ошибку в статье или какую то неточность, то прошу поправить меня!
Ресурсы, к которым я обращался в процессе поиска решения:
GroupSourcesByFolder.cmake -- репозиторий с нужной функцией от TheLartians
https://ru.stackoverflow.com/questions/556287 -- похожий цикл от пользователя Евгений
CMake 3.25 Русский (runebook.dev) -- документация CMake от машинного перевода, в котором не всегда имеются примеры к командам
Matheus Gomes - Computers, Coding and Caffeine (matgomes.com) -- Автор статей, серди которых есть о CMake командах. Хоть и на английском, но понять отсюда гораздо легче чем с официальной документации (имеются примеры)
