[CMake] Framework header hierarchy installation How-To

In this article, I’ll briefly describe how I managed to deploy a complete tree of headers with CMake.

Synopsis

The main limitation of CMake arise when the time comes to deploy the development version of your libraries, that is to say your binaries and complete header hierarchy.
No problem if all your headers are stuffed in one big fat folder, but this is not all the time the case…

As an example, have a look at the Boost libraries hierarchy which goes very deeply.

I’ve made three CMake script to be able to deploy a complete hierarchy of folders containing C or C++ headers (or whatever file), along with the static and shared library binaries.
The installation of the directory tree of headers just take one line of CMake code (with some configuration steps ahead).

For example to deploy under the folder ${IncludeBaseFolder}, all the header files contained in the directory “some/sub/folder” which is located just under the ${ProjectBaseFolder}, just type the following lines of code :

InstallHeaderHierarchy( "${ProjectBaseFolder}" "some/sub/folder" "*.hpp" "${IncludeBaseFolder}" )
InstallHeaderHierarchy( "${ProjectBaseFolder}" "some/sub/folder" "*.ipp" "${IncludeBaseFolder}" )

Usage

Here is how to use it. You typically have the following CMakeLists.txt file:

Firstly, you declar the CMake version your using or your compatible with, followed by your project name.

cmake_minimum_required(VERSION 2.8)

# Project name
project(MyProject)

Here come the configuration steps. You specify the name of your library, followed by the major, minor and patch versions. By the way you disable the logs I’ve enabled by default

# Library name and versions and disable debug logs
set( LibraryName "my_library" )
set( MajorVersion 3 )
set( MinorVersion 6 )
set( PatchVersion 5 )
set( NoFolderTreeInstallationDebug true )

At this point you can include the CMakeDeployHeaders.cmake file which contains the macro definitions for header deployment, along with the CMakeDeployLibraries.cmake file which contains the macro definitions for library deployment.
Those files include a third file which must be in the same directory: CMakeCheckVars.cmake

# Include FolderTreeInstallation
include( CMakeDeployHeaders.cmake )
include( CMakeDeployLibraries.cmake )

You can now call the InstallHeaderHierarchy macro which take four arguments:

  1. The project base folder (for example: /home/my_name/projects/MyProject). It’s the base path to copy from.
  2. The folder to deploy.
  3. The globbing pattern to match (usually “*.h”, “*.hpp”, “*.hxx”, etc.).
  4. The destination folder (usually something like /usr/local/include/my_lib-X.Y.Z).

Note: You don’t have to explicitly declare the ${ProjectBaseFolder} ${IncludeBaseFolder} variables.

InstallHeaderHierarchy( "${ProjectBaseFolder}" "sub/folder/moduleOne" "*.hpp" "${IncludeBaseFolder}" )
InstallHeaderHierarchy( "${ProjectBaseFolder}/deeper" "folder/moduleTwo" "*.h" "${IncludeBaseFolder}" )

You can now deploy the static and shared library binaries.

# Deploy binaries
DeployStaticAndSharedLibraries( ${LibraryName} ${BuildVersion} ${SoVersion} ${LibrarySourceFiles} )

That’s all for now.

CMakeDeployHeaders.cmake:

# Include variable checkings
Include( CMakeCheckVars.cmake )

# Install headers from __FolderHeaderFileList marco
macro( InstallHeaders ProjectBaseFolder DestinationIncludeBaseFolder __FolderHeaderFileList )
        # Replace back '|' by ';' (Get the list back)
        string( REPLACE "|" ";" HeaderFileList ${__FolderHeaderFileList} )
        # Go through list
        foreach( HeaderFile ${HeaderFileList} )
                string( REGEX MATCH "(.*)[/\\]" SubFolder ${HeaderFile} )
                install( FILES ${ProjectBaseFolder}/${HeaderFile} 
                        DESTINATION ${DestinationIncludeBaseFolder}/${SubFolder} )
                if( NOT DEFINED NoFolderTreeInstallationDebug )
                        message( "[InstallHeaders] Install ${ProjectBaseFolder}/${HeaderFile} \
                                 to ${DestinationIncludeBaseFolder}/${SubFolder}" )
                endif( NOT DEFINED NoFolderTreeInstallationDebug )
        endforeach(HeaderFile)
endmacro( InstallHeaders )

# GlobFiles macro
macro( GlobFiles placeHolder ProjectBaseFolder Folder Pattern )
        file( GLOB_RECURSE __FolderHeaderFileList RELATIVE "${ProjectBaseFolder}" "${Folder}/${Pattern}" )
        string( REPLACE "${ProjectBaseFolder}" "" __FolderHeaderFileList "${__FolderHeaderFileList}" )
        # Replace ';' by '|' (we don't want list here)
        string( REPLACE ";" "|" __FolderHeaderFileList "${__FolderHeaderFileList}" )
endmacro( GlobFiles )

# Install header hierarchy
macro( InstallHeaderHierarchy ProjectBaseFolder Folder Pattern IncludeBaseFolder )
        # Initialize __FolderHeaderFileList
        set( __FolderHeaderFileList " " )
        GlobFiles( ${__FolderHeaderFileList} "${ProjectBaseFolder}" "${Folder}" "${Pattern}" )
        InstallHeaders( ${ProjectBaseFolder} "${IncludeBaseFolder}" "${__FolderHeaderFileList}" )
endmacro( InstallHeaderHierarchy )

CMakeDeployLibraries.cmake:

# Include variable checkings
Include( CMakeCheckVars.cmake )

macro( DeployStaticAndSharedLibraries LibraryName BuildVersion SoVersion LibrarySourceFiles )

        # Static and Shared Libraries
        add_library( ${LibraryName}.static STATIC ${LibrarySourceFiles} )
        add_library( ${LibraryName}.shared SHARED ${LibrarySourceFiles} )

        # Set shared library's build version and .so version
        set_target_properties(
                ${LibraryName}.shared
                ${LibraryName}.shared
                PROPERTIES
                VERSION ${BuildVersion}
                SOVERSION ${SoVersion}
        )

        # Prepend suffix according to build mode
        if ( NOT DEFINED ${CMAKE_BUILD_TYPE} )
                set( OutputSuffix "" )
        elseif( ${CMAKE_BUILD_TYPE} STREQUAL "Release" )
                set( OutputSuffix "" )
        elseif ( ${CMAKE_BUILD_TYPE} STREQUAL "MinSizeRel" )
                set( OutputSuffix "-msr" )
        elseif ( ${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo" )
                set( OutputSuffix "-rwdi" )
        elseif ( ${CMAKE_BUILD_TYPE} STREQUAL "Debug" )
                set( OutputSuffix "-dbg" )
        else ( NOT DEFINED ${CMAKE_BUILD_TYPE} )
                set( OutputSuffix "" )
        endif ( NOT DEFINED ${CMAKE_BUILD_TYPE} )
        set_target_properties(
                ${LibraryName}.static
                ${LibraryName}.shared
                PROPERTIES
                OUTPUT_NAME ${LibraryName}${OutputSuffix}
                OUTPUT_NAME ${LibraryName}${OutputSuffix}
        )

        # Install shared and static libraries
        install(
                TARGETS
                ${LibraryName}.static
                ${LibraryName}.shared
                LIBRARY       DESTINATION lib
                ARCHIVE       DESTINATION lib
        )

        unset( LibrarySourceFiles )

endmacro( DeployStaticAndSharedLibraries )

CMakeCheckVars.cmake:

# WARNING: The following variable must be defined before including this file.
#
# CMAKE_PROJECT_NAME   variable must be defined. Please define it like this:
# Example:                     project( myProject )
#
# CMAKE_INSTALL_PREFIX variable must be defined. Please define it like this:
# Example:                     set( CMAKE_INSTALL_PREFIX "/usr/local" )
#
# LibraryName          variable must be defined. Please define it like this:
# Example:                     set( LibraryName "mylibrary" )
#
# MajorVersion         variable must be defined. Please define it like this:
# Example (for version 3.5.7): set( MajorVersion 3 )
#
# MinorVersion         variable must be defined. Please define it like this:
# Example (for version 3.5.7): set( MinorVersion 5 )
#
# PatchVersion         variable must be defined. Please define it like this:
# Example (for version 3.5.7): set( PatchVersion 7 )
#
#
# WARNING: The two following variables are set by default to the following values and can be overriden:
#          IncludeBaseFolder: ${CMAKE_INSTALL_PREFIX}/include/${LibraryName}-${BuildVersion}
#          ProjectBaseFolder: ${${CMAKE_PROJECT_NAME}_SOURCE_DIR}
#
# WARNING: By default the NoFolderTreeInstallationDebug variable is not defined.
#          Define it to remove those log messages. Example: set( NoFolderTreeInstallationDebug 1 )
#

# Check that mandatory variables are defined
if( NOT DEFINED CMAKE_PROJECT_NAME )
        message( FATAL_ERROR "ERROR: \${CMAKE_PROJECT_NAME} variable is not defined. Please define it.\ 
                \nExample: project( myProject )" )
elseif( NOT DEFINED CMAKE_INSTALL_PREFIX )
        message( FATAL_ERROR "ERROR: \${CMAKE_INSTALL_PREFIX} variable is not defined. Please define it.\
                \nExample: set( CMAKE_INSTALL_PREFIX \"/usr/local\" )" )
elseif( NOT DEFINED LibraryName )
        message( FATAL_ERROR "ERROR: \${LibraryName} variable is not defined. Please define it.\
                \nExample: set( LibraryName \"mylibrary\" )" )
elseif( NOT DEFINED MajorVersion )
        message( FATAL_ERROR "ERROR: \${MajorVersion} variable is not defined. Please define it.\
                \nExample (for version 3.5.7): set( MajorVersion 3 ) " )
elseif( NOT DEFINED MinorVersion )
        message( FATAL_ERROR "ERROR: \${MinorVersion} variable is not defined. Please define it.\
                \nExample (for version 3.5.7): set( MinorVersion 5 )" )
elseif( NOT DEFINED PatchVersion )
        message( FATAL_ERROR "ERROR: \${PatchVersion} variable is not defined. Please define it.\
                \nExample (for version 3.5.7): set( PatchVersion 7 )" )
endif( NOT DEFINED CMAKE_PROJECT_NAME )

if ( NOT DEFINED CHECK_VARS )
        # Initialize some variables from mandatory variables
        set( BuildVersion      "${MajorVersion}.${MinorVersion}.${PatchVersion}" )
        set( SoVersion         "${MajorVersion}" )
        set( IncludeBaseFolder "${CMAKE_INSTALL_PREFIX}/include/${LibraryName}-${BuildVersion}" )
        set( ProjectBaseFolder "${${CMAKE_PROJECT_NAME}_SOURCE_DIR}" )

        if( NOT DEFINED NoFolderTreeInstallationDebug )
                message( "NoFolderTreeInstallationDebug variable is not defined. Define it to remove those \
                        log messages. Example: set( NoFolderTreeInstallationDebug 1 )" )
                message( "LibraryName: ${LibraryName}" )
                message( "BuildVersion: ${BuildVersion}" )
                message( "SoVersion: ${SoVersion}" )
                message( "ProjectBaseFolder: ${ProjectBaseFolder}" )
                message( "IncludeBaseFolder: ${IncludeBaseFolder}" )
        endif( NOT DEFINED NoFolderTreeInstallationDebug )

        set( CHECK_VARS 1 )
endif ( NOT DEFINED CHECK_VARS )
Advertisements