Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В этой простенькой статье расскажу о проблемах, с которыми я столкнулся при изучении CMake, и о их решениях. Если кратко, то с помощью .cmake
файла от TheLartians у меня получилось из такого:
![Пример древа проекта до Пример древа проекта до](https://habrastorage.org/r/w1560/getpro/habr/upload_files/738/e86/fe3/738e86fe3391b666585716758628d6ee.png)
Сделать такое:
![Пример древа проекта послe Пример древа проекта послe](https://habrastorage.org/r/w1560/getpro/habr/upload_files/a21/255/ff6/a21255ff6f9c07460a2732b61db9a18b.png)
Для кого и зачем эта статья?
В первую очередь данная статья ориентирована на новичков в CMake, которые как и я изучают его по документациям и туториалам в интернете. К сожалению, в ру сегменте мне не удалось найти точную и конкретную информацию по настройке структуры проекта (Возможно я плохо искал, буду рад увидеть в комментариях ссылки на источники). Потому пришлось обратится к англоязычным ресурсам и статьям, которые понять мне(плохо знающему данный язык) было сложновато. Пишу я это статью с желанием помочь таким же как и я, дабы люди могли больше уделять внимание именно разработке, а не поиску решения , с виду, простых проблем.
Подмечу что мой пример и мои проекты хранят заголовочные файлы вместе с CPP файлами, то есть у меня нету в проектах директории Include, где обычно располагают хейдеры. Для моих пока что мелких проектов создавать библиотеки не имеет смысла. Имейте это ввиду!
Приступим к решению проблемы
Допустим имеется следующая структура проекта :
ExampleFolderStructure
.
├── CMakeLists.txt
└── src
├── Game
│ ├── Game.cpp
| ├── Game.h
| └── GameObjects
│ ├── IGameObject.cpp
| └── IGameObject.h
└── ResourceManager
├── ResourceManager.cpp
└── ResourceManager.h
Cначала создадим таргет со всеми исполняемыми и заголовочными файлами. Сделать это можно либо введя все файлы вручную, либо воспользовавшись командой file
. Воспользуемся вторым подходом:
cmake_minimum_required(VERSION 3.5)
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 остались ALL_BUILD и ZERO_CHECK были автоматический добавлены в папку по умолчанию, но Header Files и Source Files остались](https://habrastorage.org/r/w1560/getpro/habr/upload_files/7a0/5f1/054/7a05f10542426e65b18730eb63a33f45.png)
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.5)
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, мы получим вот такой вот результат:
![Кра-со-та! Кра-со-та!](https://habrastorage.org/r/w1560/getpro/habr/upload_files/34e/7fd/1c2/34e7fd1c209f6c3ff4fd7d5c3d12cd5e.png)
Рекомендую подебагерить и поизучать код самостоятельно с помощью команды 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
.
Итоги
Теперь нам удалось создать древовидную структуру хранения файлов, как и в самом проводнике, что значительно повышает читабельность проекта и ускоряет навигацию по нему. До того, как я прибегнул к такому решению, мой проект выглядел следующим образом:
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/f89/8d4/f1a/f898d4f1a1d9a62e45fa32f00e23e2e1.png)
А уже после вот так:
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/a7f/a80/411/a7fa8041190b084bb0618d9aa4f2e86e.png)
Начиная изучать 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 командах. Хоть и на английском, но понять отсюда гораздо легче чем с официальной документации (имеются примеры)