В этой простенькой статье расскажу о проблемах, с которыми я столкнулся при изучении 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
(в данном случае поиск путей к файлам определенного типа), второй параметр за переменную, куда всё будет записано, а третий аргумент отвечает за расположение директории, в которой нужно искать файлы определённого расширения. Уже теперь наш проект выглядит куда симпатичнее
Затем опционально можно обернуть наш таргет 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 командах. Хоть и на английском, но понять отсюда гораздо легче чем с официальной документации (имеются примеры)
Sazonov
Посмотрите вторую сигнатуру source_group: https://cmake.org/cmake/help/latest/command/source_group.html
Есть мнение что вы изобрели велосипед.
F1Soda Автор
Вы правы! Признаю, допустил грубую ошибку -- не прочитал полностью документацию. Я видел вторую сигнатуру и пытался ею сам воспользоваться, но после первой же неудачной попытки отбросил старания и продолжил искать другие решения. А как оказалось, большая часть статей и репозиториев с github использовали версию с циклом foreach. Я добавлю ваше замечание к статье. Большое спасибо!
Sazonov
У нас на одном проекте было ограничение по версии cmake и из-за этого приходилось делать что-то похожее на ваше решение. Только мы вообще с нуля, руками, генерировали файл с фильтрами для студии.