CMake經驗

命令

源文件收集

一般情況下,我們通過逐個列出的方式,設定源文件集(set(SRC_LIST src/main.c src/other.c))。這樣的好處是可以明確控制那些文件會被加入到工程中。 但是有些時候,如果源文件較多,一個一個列出來的話可能會有些麻煩,對於這種情況,可以使用CMake提供的file命令來自動收集源文件。使用方法如下。

file(GLOB_RECURSE SRC_CORE src/*.c)

通過上面這行代碼,會生成一個源文件列表,每一項是一個源文件的全路徑。可以通過下面的代碼來剔除一些不想加入工程的源文件。

foreach(rm_file ${SRC_CORE})
    string(REGEX MATCH ".*/filename1.c|.*/filename2.c" need_remove_file ${rm_file})
    if(need_remove_file)
        list(REMOVE_ITEM SRC_CORE ${need_remove_file})
    endif(need_remove_file)
endforeach(rm_file)

上面使用了CMake的正則表達式,匹配兩個文件,分別爲.*/filename1.c.*filename2.c,其中.*代表了任意字符串,filename1.c是文件名。

同一個工程同時生成靜態庫和動態庫

有了如標題所述的需求,在網上搜索了一下,發現大部分博客都是講通過設置OUTPUT_NAME,使兩個工程(一個動態庫,一個靜態庫)輸出同名二進制文件。但是我不希望同樣的源碼,要創建兩個工程。 可以使用下面的方法爲使用一個工程同時生成動態庫和靜態庫。

# 添加控制選項
# BUILD_SHARED_LIBS 是CMake內置變量
# 針對WIN32可以

if(WIN32)
    option(BUILD_SHARED_LIBS "Build Shared libs" ON)
    option(BUILD_AS_DLL "Build as dll" ${BUILD_SHARED_LIBS})
endif()

add_library(libHello ${SRC_LIST})
target_link_libraries(libHello ${EXTRA_LIB})

if(BUILD_AS_DLL)
    set_target_properties(libHello PROPERTIES COMPILE_DEFINITIONS BUILD_AS_DLL)
endif()

設置頭文件目錄和靜態庫

很多時候,我們會使用第三方庫,因此,我們要在工程裏面包含第三方庫的頭文件,鏈接第三方提供的靜態鏈接庫(如果有的話)。

# 包含頭文件
# include_directories 具有繼承性
# 當前 CMakeLists 裏面包含的目錄,會被 add_subdirectory 裏面的 CMakeLists 繼承
include_directories ( "D:/Program Files (x86)/Lua/include" )

# 庫目錄
# eg. 鏈接 Linux 下的 X11
link_directories(/usr/include/X11)
SET(SYS_LIB_LIST "x11")

# 鏈接庫
set(LUA_LIB "D:/Program Files (x86)/Lua/lib/lua51.lib")
target_link_libraries(target ${LUA_LIB})

生成配置頭文件

一般可以用CMake來進行平臺檢測,那麼CMake的檢測結果如何告訴C++工程呢?CMake提供了configure_file命令。 configure_file命令輸入一個文本文件,處理文本文件中包含的CMake的指令,然後生成一個文本文件。

configure_file(
    "${PROJECT_SOURCE_DIR}/platform/config.h.in"
    "${PROJECT_BINARY_DIR}/config.h"
    )
/*
    config.h.in 文件
*/
#cmakedefine PLATFORM_WIN32
#cmakedefine PLATFORM_LINUX

如果 CMakeLists.txt 中定義了PLATFORM_WIN32變量,那麼#cmakedefine PLATFORM_WIN32就會變成C++的宏定義語句#define PLATFORM_WIN32

生成Visual Studio篩選器

可以通過一個宏來爲收集的文件建立篩選器。代碼如下:

# file(GLOB_RECURSE all_files *.*)
# create_filters(all_files)
# add_executable(app ${all_files})

macro(create_filters source_files)
    if(MSVC)
        # 獲取當前目錄
        set(current_dir ${CMAKE_CURRENT_SOURCE_DIR})
        foreach(src_file ${${source_files}})
            # 求出相對路徑
            string(REPLACE ${current_dir}/ "" rel_path_name ${src_file})
            # 刪除相對路徑中的文件名部分
            string(REGEX REPLACE "(.*)/.*" \\1 rel_path ${rel_path_name})
            # 比較是否是當前路徑下的文件
            string(COMPARE EQUAL ${rel_path_name} ${rel_path} is_same_path)
            # 替換成Windows平臺的路徑分隔符
            string(REPLACE "/" "\\" rel_path ${rel_path})
            if(is_same_path)
                set(rel_path "\\")
            endif(is_same_path)

            # CMake 命令
            source_group(${rel_path} FILES ${src_file})
        endforeach(src_file)
    endif(MSVC)
endmacro(create_filters)

編譯選項

Linux使用 C++ 11

在Linux下編譯下面的代碼時出錯。

typedef std::map<key, value> Dict_Map;
Dict_Map::iterator it = DictMap.begin();
while(it != DictMap.end())
{
    it = DictMap.erase(it);
}

出錯提示是erase函數沒有返回值。 查看源碼,發現#if __cplusplus >= 201103L時,iterator erase(const_iterator __position)。否則,void erase(iterator __position)。 改成使用 C++ 11 就好了。

# Use C++11 in linux
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=gnu++0x")

MSVC 禁用特定警告

一般情況下,不要禁用任何警告,而應該積極解決每一個警告。 但是有些情況下,當你充分了解某些警告可能造成的後果時,也可以禁用該警告。 比如要禁用警告4819(該文件包含不能在當前代碼頁中表示的字符),可以採用下面的語句:

if(MSVC)

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819")

endif()

調試運行

Visual Studio設置工作目錄

使用Visual Studio開發的時候,假設編譯構建生成的程序在binarypath/bin/debug目錄下。我們調試時,會有一些數據要使用,假設這些數據在binarypath/data目錄下,如果程序查找數據文件時,只在當前目錄下查找,那麼在這種情況下,就無法找到數據。要解決這個問題,可以在Visual Studio中設置工作目錄,這樣程序就能找到數據文件。 Visual Studio的工作目錄設置是保存在projectname.vcxproj.user文件中,CMake沒有提供專門的命令來設置工作目錄,但是我們可以通過類似配置文件的方式來手動生成projectname.vcxproj.user文件。 perconfig.vcxproj.user.in

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='@USERFILE_CONFIGNAME@|@USERFILE_PLATFORM@'">
    <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
    <LocalDebuggerCommandArguments>@USERFILE_COMMAND_ARGUMENTS@</LocalDebuggerCommandArguments>
    <LocalDebuggerWorkingDirectory>@USERFILE_WORKING_DIRECTORY@</LocalDebuggerWorkingDirectory>
</PropertyGroup>

vcxproj.user.in

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="@USERFILE_VC_VERSION@" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
@USERFILE_CONFIGSECTIONS@
</Project>

CMake 模塊

file(READ "${_launchermoddir}/perconfig.vcxproj.user.in" _perconfig)
set(USERFILE_CONFIGSECTIONS)
# 對於每一個配置(Debug, Release, RelWithDebInfo, MinSizeRel),生成對應的配置項
foreach(USERFILE_CONFIGNAME ${CMAKE_CONFIGURATION_TYPES})
    string(CONFIGURE "${_perconfig}" _temp @ONLY ESCAPE_QUOTES)
    string(CONFIGURE
        "${USERFILE_CONFIGSECTIONS}${_temp}"
        USERFILE_CONFIGSECTIONS
        ESCAPE_QUOTES)
endforeach()

configure_file("${_launchermoddir}/vcxproj.user.in"
    ${VCPROJNAME}.vcxproj.user
    @ONLY)

perconfig.vcxproj.user.in中出現的@USERFILE_CONFIGNAME@@USERFILE_PLATFORM@@USERFILE_PLATFORM@@USERFILE_COMMAND_ARGUMENTS@@USERFILE_WORKING_DIRECTORY@等,都是在CMake模塊中預先設置好的變量,通過string(CONFIGURE "${_perconfig}" _temp @ONLY ESCAPE_QUOTES)語句填到配置裏面去的。

拷貝運行時庫

對於需要動態鏈接的庫,一般是直接在可執行程序目錄下查找的,所以對於項目中使用的第三方動態鏈接庫,我們需要在用CMake構建項目的時候,將這些動態鏈接庫拷貝到最終可執行文件目錄下。

拷貝動態鏈接庫,用於調試時運行。

macro(copy_dll depdir dllname)
    foreach(configuration ${CMAKE_CONFIGURATION_TYPES})
        # 對應於不同的構建版本
        set(dllpath "${depdir}/bin/${configuration}/${dllname}")
        if(EXISTS ${dllpath})
            configure_file(${dllpath} ${CMAKE_BINARY_DIR}/bin/${configuration}/${dllname} COPYONLY)
        endif()
    endforeach()
endmacro()

安裝動態鏈接庫,用於最終應用程序安裝部署。

macro(install_dll depdir dllname)
    foreach(configuration ${CMAKE_CONFIGURATION_TYPES})
        set(dllpath "${depdir}/bin/${configuration}/${dllname}")
        if(EXISTS ${dllpath})
            install(FILES ${dllpath} DESTINATION bin/${configuration} CONFIGURATIONS ${configuration})
        endif()
    endforeach()
endmacro()

安裝

設置安裝路徑前綴

使用install命令安裝項目的時候,如果路徑給出的是全路徑,那麼會直接使用這個路徑,如果給出的是相對路徑,那麼會加上CMAKE_INSTALL_PREFIX,默認情況下這個值爲C:/Program Files (x86)。可以直接在CMakeLists裏面設置這個值,也可以在CMake GUI裏面設置。

set(CMAKE_INSTALL_PREFIX "D:/Program Files (x86)/" CACHE PATH "Project install directory" FORCE)

根據配置設置安裝目錄

CMake預定義了四種編譯配置: Debug、Release、RelWithDebInfo、MinSizeRel。大多數時候,我們只會安裝Release版本。但是在某些情況下,比如A項目鏈接B項目的靜態庫,當A項目採用Debug編譯時,需要鏈接B項目的Debug版本,當A項目採用Release編譯時,需要鏈接B項目的Release版本。

假設我們使用了link_directories(${PROJECT_BINARY_DIR}/lib)命令設置了靜態庫查找目錄,那麼在Visual Studio中,實際查找目錄是/projectpath/lib/projectpath/lib/$(Configuration)$(Configuration)表示編譯配置。

因此,我們在設置安裝目錄時,也可以採用lib/Debuglib/Release這種結構。使用下面的代碼可以完成這個功能。

foreach(lib_file platform)
    install(TARGETS ${lib_file}
        RUNTIME DESTINATION "bin/Debug" CONFIGURATIONS Debug
        LIBRARY DESTINATION "lib/Debug"  CONFIGURATIONS Debug
        ARCHIVE DESTINATION "lib/Debug" CONFIGURATIONS Debug
        )
    install(TARGETS ${lib_file}
        RUNTIME DESTINATION "bin/Release" CONFIGURATIONS Release
        LIBRARY DESTINATION "lib/Release"  CONFIGURATIONS Release
        ARCHIVE DESTINATION "lib/Release" CONFIGURATIONS Release
        )
    install(TARGETS ${lib_file}
        RUNTIME DESTINATION "bin/RelWithDebInfo" CONFIGURATIONS RelWithDebInfo
        LIBRARY DESTINATION "lib/RelWithDebInfo"  CONFIGURATIONS RelWithDebInfo
        ARCHIVE DESTINATION "lib/RelWithDebInfo" CONFIGURATIONS RelWithDebInfo
        )
    install(TARGETS ${lib_file}
        RUNTIME DESTINATION "bin/MinSizeRel" CONFIGURATIONS MinSizeRel
        LIBRARY DESTINATION "lib/MinSizeRel"  CONFIGURATIONS MinSizeRel
        ARCHIVE DESTINATION "lib/MinSizeRel" CONFIGURATIONS MinSizeRel
        )
endforeach()

工具

作爲一個項目構建工具,CMake用一種優雅的方式,描述了項目構建的步驟。 但是對於大型項目而言,光是使用CMake簡化項目構建過程還是不夠的,還需要配備一套程序配合CMake來完成項目構建的過程。

編譯加速

CMake只完成了從源文件到建立項目工程的過程,而真正編譯,則還是要依賴於特定的編譯工具,比如Visual Studio、gcc等。 對於大型工程而言,工程的編譯時間也是一件值得優化的事。目前,有一些工具可以加速基於CMake構建的工程的編譯過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章