Сохранение древовидной структуры в Visual Studio с CMake или создание папок в проекте

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Пример древа проекта до
Пример древа проекта до

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

Пример древа проекта послe
Пример древа проекта послe

Для кого и зачем эта статья?

В первую очередь данная статья ориентирована на новичков в 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 остались

Затем опционально можно обернуть наш таргет 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, мы получим вот такой вот результат:

Кра-со-та!
Кра-со-та!

Рекомендую подебагерить и поизучать код самостоятельно с помощью команды 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 командах. Хоть и на английском, но понять отсюда гораздо легче чем с официальной документации (имеются примеры)

Источник: https://habr.com/ru/articles/754272/


Интересные статьи

Интересные статьи

Предлагаю ознакомиться с ранее размещенными материалами по проекту StarLink (SL): ‣ Часть 30. Сравнение сервиса StarLink с сервисами других операторов ШПД ‣ Часть 31. Описание антенны Ка-диапазона...
Гиперконвергентная инфраструктураГиперконвергентная инфраструктура (HCI) - современный подход к инфраструктуре. Это комплексное решение, которое обеспечивает производительность и емкость данных с испо...
Это вторая часть статьи, посвященной универсальному сервису подписания для инфраструктуры Госуслуг. Первая часть статьи была посвящена GO-части нового сервиса, в которую ...
Приветствую во второй публикации цикла статей, посвященному Cisco ISE. В первой статье  были освещены преимущества и отличия Network Access Control (NAC) решений от стандартных ...
Apache Dubbo — один из самых популярных Java проектов на GitHub. И это неудивительно. Он был создан 8 лет назад и широко применяется как высокопроизводительная RPC среда. Конечно, большинство о...