Search code examples
cmakecross-platformexternal-project

How do I use CMake ExternalProject_Add or alternatives in a cross-platform way?


I would like to build a third-party project that already has CMake as part of my project's CMake strips. ExternalProject_Add is for this purpose, but I have found it can only be made to work with a specific generator, and I wanted it to work on many platforms easily.

For example, here is my external project with an added script for zlib, which has its own CMakeLists.txt:

set(USE_PROJECT_CMAKE_MODULE_PATH "-DCMAKE_MODULE_PATH=${MAKE_MODULE_PATH}")
ExternalProject_Add(ZLIB
                    SOURCE_DIR ${CMAKE_SOURCE_DIR}/external/zlib
                    DOWNLOAD_COMMAND ""
                    UPDATE_COMMAND ""
                    CMAKE_ARGS
                       -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
                       -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
                       -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}
                       -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
                       ${USE_PROJECT_CMAKE_MODULE_PATH}
                    INSTALL_COMMAND "")

ExternalProject_Add_Step(ZLIB installInternally
                         COMMAND cd <BINARY_DIR> && make install
                         DEPENDEES install
                         ALWAYS 1)
ExternalProject_Get_Property(ZLIB install_dir)

if(UNIX)
    set(ZLIB_NAME libz)
else(UNIX)
    set(ZLIB_NAME zlib)
endif(UNIX)

add_library(zlib UNKNOWN IMPORTED)
set_property(TARGET zlib PROPERTY IMPORTED_LOCATION ${install_dir}/lib/${ZLIB_NAME}.a)
set(ZLIB_LIBRARIES zlib)
set(ZLIB_LIBRARIES_OPTIONAL ${ZLIB_LIBRARIES})
set(ZLIB_DIR ${install_dir} CACHE INTERNAL "zlib ROOT dir")
set(ZLIB_INCLUDE_DIRS ${install_dir}/include CACHE INTERNAL "zlib include dirs")
set(ZLIB_DEFINES "-msse2 -mfpmath=sse" CACHE INTERNAL "zlib defines")

The problem with this is that it works with make, but not with Xcode or Visual Studio. Perhaps there is some way to take the CMake build commands passed to my project and forward them to ExternalProject_Add.

How can I write ExternalProject_Add calls in a cross-platform way with minimal code complexity, or is there a better alternative?


Solution

  • Problems

    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    

    This is enough for single-configuration projects. But for Xcode and Visual Studio, you need to set CMAKE_CONFIGURATION_TYPES plus call build . --config at the build stage. See my answer.

    COMMAND cd <BINARY_DIR> && make install
    

    This will work only for Makefile generators of course. To be cross-platform you can use:

    --build . --target install --config inside INSTALL_COMMAND of ExternalProject_Add.

    Take a look at this template file, and in particular the following lines:

    ExternalProject_Add(
        "${current_project}"
        URL
        @HUNTER_PACKAGE_URL@
        URL_HASH
        SHA1=@HUNTER_PACKAGE_SHA1@
        DOWNLOAD_DIR
        "@HUNTER_PACKAGE_DOWNLOAD_DIR@"
        SOURCE_DIR
        "@HUNTER_PACKAGE_SOURCE_DIR@"
        INSTALL_DIR
        "@HUNTER_PACKAGE_INSTALL_PREFIX@"
            # Not used, just avoid creating Install/<name> empty directory
        BUILD_COMMAND ""
            # This command is empty because all necessary targets will
            # be built on install stage
        CMAKE_ARGS
        "-G@CMAKE_GENERATOR@"
        "-C@HUNTER_CACHE_FILE@"
        "-C@HUNTER_ARGS_FILE@"
        "-D${postfix_name}=${${postfix_name}}"
        "-DCMAKE_BUILD_TYPE=${configuration}"
        "-DCMAKE_CONFIGURATION_TYPES=${configuration}"
        "-DCMAKE_INSTALL_PREFIX=@HUNTER_PACKAGE_INSTALL_PREFIX@"
        "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}"
        INSTALL_COMMAND
            "@CMAKE_COMMAND@"
            --build .
            --target install
            --config ${configuration}
            --
            ${jobs_option}
    )
    

    Alternative

    or is there a better alternative?

    Have you seen Hunter?

    You can add zlib just like this:

    hunter_add_package(ZLIB)
    find_package(ZLIB CONFIG REQUIRED)
    target_link_libraries(... ZLIB::zlib)
    

    This code works everywhere. Third party dependencies will be downloaded automatically in the configuration step. Example of building with different generator/toolchains (build.py is just a CMake wrapper that sets CMAKE_TOOLCHAIN_FILE and -G/-B):

    build.py --toolchain mingw --config Release # MinGW Makefiles
    build.py --toolchain vs-12-2013 --config Debug # Visual Studio 12 2013
    build.py --toolchain xcode --config Release # Xcode
    build.py --toolchain libcxx --config Release # Makefile with -stdlib=libc++ toolchain
    build.py --toolchain ios-8-2 --config Release # Xcode with iOS SDK 8.2 toolchain
    

    You got full control what options, build types or number of jobs you want to have while building third-party packages. For instance, this is how you can build four types, Debug, Release, MinSizeRel, and RelWithDebInfo for zlib and link MinSizeRel to the current project:

    > build.py --toolchain xcode --verbose --config MinSizeRel --fwd "HUNTER_CONFIGURATION_TYPES=Release;Debug;MinSizeRel;RelWithDebInfo"
    /.../clang  /.../lib/libz-MinSizeRel.a ... -o /.../_builds/xcode/MinSizeRel/foo
    
    > ls -la /.../.hunter/_Base/d1232c0/326318e/37e4682/Install/lib/libz*
       99056 /.../.hunter/_Base/d1232c0/326318e/37e4682/Install/lib/libz-MinSizeRel.a
      307872 /.../.hunter/_Base/d1232c0/326318e/37e4682/Install/lib/libz-RelWithDebInfo.a
      109536 /.../.hunter/_Base/d1232c0/326318e/37e4682/Install/lib/libz.a
      258904 /.../.hunter/_Base/d1232c0/326318e/37e4682/Install/lib/libzd.a