CMake и статические ресурсы

Дисклеймер: статья написана разработчиком некоторого кода на Java (и в разработке использовалась соответсвующая экосистема, вроде системы сборки Maven), соответственно, у автора выработался некоторый набор привычек и ожиданий, который может показаться странным человеку, имеющему опыт работы с CMake.

В ходе разработки некоторого проекта на C++ у меня возникла потребность в использовании в коде некоторых ресурсов из файловой системы, не являющихся кодом (допустим, это были некоторые конфигурационные файлы для исходного кода — в данном контексте это не принципиально). Подразумевался некоторый набор файлов для разработки/тестирования, который затем может быть изменен при поставке итогового продукта. Как разработчик на Java, я привык помещать подобные файлы в каталог resources (являющийся каталогом по умолчанию для подобного рода файлов в плагине maven-resources-plugin системы сборки Maven), и при необходимости конфигурировать maven-resources-plugin для фильтрации этих ресурсов (например, в продукционной и тестовой сборках). И я был не очень приятно удивлен, когда обнаружил, что в используемой системе сборки подобная роскошь отсутствует. Пришлось окунуться с головой в ответы на StackOverflow документацию в поисках решения.

Превое, на что можно наткнуться при поиске — команда file. К сожаленю, она исполняется лишь при запуске cmake'а, что не подходит для задачи — содержимое файлов ресурсов может меняться, и при этом они должны быть скопированы в нужное место при следующей сборке с помощью make'а. Потому поиск пришлось продолжить.

Следующее, что было найдено — это команда configure_file. На первый взгляд, она делает то, что нужно — создает зависимость от файла, что приведет к его копированию в указанное место при запуске make'а (в случае, если он был изменен). Команда может даже немного больше ‒ например, заменять в копируемом файле переменные вида ${VAR} и @VAR@. А вот чего она не может — это копировать целые каталоги.

Учитывая специфику команды configure_file, возникла необходимость в написании собственного макроса CMake (благо, в нем есть такая возможность), а для этого нужно хоть как-то формализовать свои требования к нему.

  1. Макросу передается два пути — исходный и целевой каталоги.
  2. Относительный путь исходного каталога разрешается относительно исходного каталога (переменная CMAKE_CURRENT_SOURCE_DIR).
  3. Относительный путь целевого каталога разрешается относительно каталога с скомпилированными файлами (CMAKE_CURRENT_BINARY_DIR).
  4. Каталог с ресурсами копируется рекурсивно.
  5. Изменение содержимого файлов должно приводить к их повторному копированию при следующем запуске make'а.
  6. Изменения в структуре каталога ресурсов (добавление новых файлов/каталогов и т.п.) обрабатываются только при запуске cmake'а.
  7. Никаких требований по обработке символических ссылок не выдвигается.

В итоге задача сводится к рекурсивному обходу каталога ресурсов и создания зависимостей от каждого файла в нем с помощью configure_file. В результате был получен следующий макрос:

macro(all_subdirs result source)
    set(dirlist "")
    set(res "")
    list(APPEND dirlist ${source})
    list(LENGTH dirlist len)
    while (${len} GREATER 0)
        list(GET dirlist 0 curdir)
        file(GLOB children RELATIVE ${source} ${curdir}/*)
        foreach(child ${children})
            if(IS_DIRECTORY ${source}/${child})
                list(APPEND dirlist ${source}/${child})
                list(APPEND res ${child})
            endif()
        endforeach()
        list(REMOVE_AT dirlist 0)
        list(LENGTH dirlist len)
    endwhile()
    set(${result} ${res})
endmacro()

#Copy all files from specified source to target
#Source relative path is treated with respect to the value of CMAKE_CURRENT_SOURCE_DIR
#Target relative path is treated with respect to the value of CMAKE_CURRENT_BINARY_DIR
#If target already exists and it is not CMAKE_CURRENT_BINARY_DIR, it will be removed
macro(configure_resources source target)
    if (NOT IS_ABSOLUTE ${source})
        set(abs_source ${CMAKE_CURRENT_SOURCE_DIR}/${source})
    else()
        set(abs_source ${source})
    endif()
    if (NOT IS_ABSOLUTE ${target})
        set(abs_target ${CMAKE_CURRENT_BINARY_DIR}/${target})
    else()
        set(abs_target ${target})
    endif()
    if (NOT EXISTS ${abs_source})
        message(SEND_ERROR "Source resources do not exist")
    else()
        if (NOT IS_DIRECTORY ${abs_source})
            message(SEND_ERROR "Source is not directory")
        else()
            if (EXISTS ${abs_target})
                message("Clean target")
                if (NOT ${abs_target} STREQUAL ${CMAKE_CURRENT_BINARY_DIR})
                    file(REMOVE_RECURSE ${abs_target})
                endif()
            endif()
            file(MAKE_DIRECTORY ${abs_target})
            all_subdirs(subs ${abs_source})
            list(INSERT subs 0 ".")
            foreach(sub ${subs})
                set(curdir ${abs_source}/${sub})
                set(targetdir ${abs_target}/${sub})
                file(MAKE_DIRECTORY ${targetdir})
                file(GLOB contents RELATIVE ${curdir} ${curdir}/*)
                foreach(child ${contents})
                    if (NOT IS_DIRECTORY ${curdir}/${child})
                        configure_file(${curdir}/${child} ${targetdir}/${child} COPYONLY)
                    endif()
                endforeach()
            endforeach()
        endif()
    endif()
endmacro()

Спасибо за внимание.

Засим откланиваюсь, прощайте.