This blog is part #2 in the series of trying out different package managers to bootstrap a cmake project. Checkout part #1 about Bootstrapping a vcpkg-based cmake project in Visual Studio. Part #3 is about bootstrapping Hunter-based cmake project in Linux and Visual Studio. The cmake code in the previous post works well on Linux too. After all, both cmake and vcpkg are designed for cross-platform build management. So what's new here?
This time around we'll get the same project off the ground in both Linux and Windows with cmake proper. Last time, the cmake script
Feedback from Carlos ORyan (Google) forms the basis of this blog post. It would be more accurate to say that I'm downright stealing the cmake-vcpkg integration scripts he shared with me. They are open-source and available at google-cloud-cpp/super. I've copied them nearly verbatim to my vcpkg_cmake_blog branch for ease of use and long term stability of the hyperlinks. Thanks Carlos!
The objective is the same: bootstrap a vcpkg-based cmake project. The mechanics are much more sophisticated and feel idiomatic cmake. Let's get started.
The
We're including the files under cmake directory as "modules" and simply invoking them using
The vcpkg-download is a separate cmake project. The CMakeLists.txt for this project is created while generating the build files for the driver project. I.e., It allows every project to bootstrap it's own vcpkg repository. This may or may not be desirable. For smaller project it might be an overkill. For large projects where controlling the exact library version separate from vcpkg repository HEAD is desirable, one might want a dedicated vcpkg instance. Here's the ExternalProject vcpkg_download.
The vcpkg_download function spits out and runs this project (with another invocation of cmake) only if needed. I ended up passing additional flags to cmake on Windows. Having to pass additional flags like
The main differences between part #1 and this cmake project are the following.
This time around we'll get the same project off the ground in both Linux and Windows with cmake proper. Last time, the cmake script
CMakeLists.txt
felt like a poorly written bash script. Since that blogpost, I received a lot of feedback.
Feedback from Carlos ORyan (Google) forms the basis of this blog post. It would be more accurate to say that I'm downright stealing the cmake-vcpkg integration scripts he shared with me. They are open-source and available at google-cloud-cpp/super. I've copied them nearly verbatim to my vcpkg_cmake_blog branch for ease of use and long term stability of the hyperlinks. Thanks Carlos!
The objective is the same: bootstrap a vcpkg-based cmake project. The mechanics are much more sophisticated and feel idiomatic cmake. Let's get started.
Cmake Project Structure
vcpkg_test ├── cmake │ ├── AutoVcpkg.cmake │ └── VcpkgBootstrap.cmake ├── CMakeLists.txt ├── include │ └── driver.h ├── src │ └── driver.cpp └── test └── driver_test.cppThere're two more files under the cmake directory. These are cmake scripts designed to download, install, configure vcpkg instances in both Linux and Windows. They also expose suitable cmake function for use to use in
CMakeLists.txt
. This integration is much nicer (but also complex).
The
CMakeLists.txt
looks as follows.
cmake_minimum_required (VERSION 3.12) set(MY_PROJECT_DEPENDENCIES boost-core boost-optional boost-filesystem) # This section of cmake is using AutoVcpkg to download, install, and configure vcpkg. list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") include(AutoVcpkg) vcpkg_install(${MY_PROJECT_DEPENDENCIES}) message(STATUS "CMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}") # The CMakeLists from this point on is the same as that of part 1. project (vcpkg_test CXX) set(CMAKE_CXX_STANDARD 17) find_package(Boost 1.67 REQUIRED COMPONENTS filesystem) add_executable(driver src/driver.cpp) target_include_directories(driver PUBLIC ${Boost_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/include ) target_link_libraries(driver ${Boost_LIBRARIES}) enable_testing() include(CTest) add_executable(driver_test ${PROJECT_SOURCE_DIR}/test/driver_test.cpp) add_test(NAME driver COMMAND driver_test)
find_package
finds and loads settings from an external project (package). Boost_FOUND
will be set to indicate whether the Boost package was found. add_executable
simply adds a target named driver to be built from the sources (src/driver.cpp
). The Boost library dependencies are specified next for the driver
target. First, a set of include directories are specified. Next, a set of libraries are specified. Note that boost-filesystem must be linked to driver program. Hence, target_link_libraries
is essential. The variables Boost_INCLUDE_DIRS
, Boost_LIBRARIES
are set by find_package
(only upon success).
vcpkg_install
Here's the full code of AutoVcpkg.cmake. Here's the github branch vcpkg_cmake_blog_idiomatic.We're including the files under cmake directory as "modules" and simply invoking them using
vcpkg_install
. The code is mostly self-explanatory. If you are new to cmake, you might have to stare at it for a while though.
The vcpkg-download is a separate cmake project. The CMakeLists.txt for this project is created while generating the build files for the driver project. I.e., It allows every project to bootstrap it's own vcpkg repository. This may or may not be desirable. For smaller project it might be an overkill. For large projects where controlling the exact library version separate from vcpkg repository HEAD is desirable, one might want a dedicated vcpkg instance. Here's the ExternalProject vcpkg_download.
cmake_minimum_required(VERSION 3.12) project(vcpkg-download) include(ExternalProject) ExternalProject_Add(vcpkg GIT_REPOSITORY @AUTO_VCPKG_GIT_REPOSITORY@ # GIT_TAG 52870c7595a63ade069ae51d5f4ee3a85fe4123f # TODO: Debug this GIT_SHALLOW ON SOURCE_DIR @AUTO_VCPKG_ROOT@ PATCH_COMMAND "" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" LOG_DOWNLOAD ON LOG_CONFIGURE ON LOG_INSTALL ON)So instead of simply forking off and launching
git clone
directly from cmake, this external project allows a plethora of options and configure the download step.
The vcpkg_download function spits out and runs this project (with another invocation of cmake) only if needed. I ended up passing additional flags to cmake on Windows. Having to pass additional flags like
CMAKE_EXE_LINKER_FLAGS, CMAKE_C_COMPILER, and CMAKE_CXX_COMPILER
(from parent to the nested invocation of cmake) indicates that cmake integration with Visual Studio is still rough on the edges. Here's a snippet.
function (vcpkg_download) if (DEFINED AUTO_VCPKG_ROOT) return() endif () set(AUTO_VCPKG_ROOT "${CMAKE_BINARY_DIR}/vcpkg") # Generate the vcpkg_download project if necessary. file(WRITE "${CMAKE_BINARY_DIR}/vcpkg-download/CMakeLists.txt" "${vcpkg_download_contents}") if(WIN32) get_filename_component(VC_COMPILER_PATH ${CMAKE_C_COMPILER} DIRECTORY) set(VCRT_LIB_PATH "${VC_COMPILER_PATH}/../../../lib/x86") execute_process(COMMAND "${CMAKE_COMMAND}" "-H${CMAKE_BINARY_DIR}/vcpkg-download" "-B${CMAKE_BINARY_DIR}/vcpkg-download" "-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}" "-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}" "-DCMAKE_EXE_LINKER_FLAGS=/LIBPATH:\"${VCRT_LIB_PATH}\"") execute_process(COMMAND "${CMAKE_COMMAND}" "--build" "${CMAKE_BINARY_DIR}/vcpkg-download") else() # Linux here. endif()If the previous step does not succeed in building vcpkg successfully (i.e., if
AUTO_VCPKG_EXECUTABLE
is undefined), there's plan B. The plan B is to do pretty much fork off a child cmake process and run vcpkg bootstrap.sh or bootstrap.bat directly. We saw a very simple version of it in part #1.
function (vcpkg_bootstrap) find_program(AUTO_VCPKG_EXECUTABLE vcpkg PATHS ${AUTO_VCPKG_ROOT}) if (NOT AUTO_VCPKG_EXECUTABLE) execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_LIST_DIR}/cmake/VcpkgBootstrap.cmake" "${AUTO_VCPKG_ROOT}") execute_process(COMMAND ${CMAKE_COMMAND} -P "${AUTO_VCPKG_ROOT}/VcpkgBootstrap.cmake" WORKING_DIRECTORY ${AUTO_VCPKG_ROOT}) endif () endfunction () ###### VcpkgBootstrap.cmake file find_program(VCPKG_EXECUTABLE vcpkg PATHS "${CMAKE_CURRENT_LIST_DIR}") if (NOT VCPKG_EXECUTABLE) if (WIN32) execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/bootstrap-vcpkg.bat" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}") else () execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/bootstrap-vcpkg.sh" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}") endif () endif ()At this point we've covered the gist. There are a lot of new things I learned about cmake.
The main differences between part #1 and this cmake project are the following.
- vcpkg is cloned from the github repository, compiled, and bootstrapped in the cmake
binary
directory. The directory you use for out-of-source builds (e.g., build). Previously, vcpkg is cloned, compiled, and bootstrapped in$ENV{HOMEDRIVE}$ENV{HOMEPATH}/vcpkg_cpptruths
- The
vcpkg-download
project is a real cmake project that generates aMakefile
for bootstrapping vcpkg. On Windows, it generates a solution file under$ENV{HOMEDRIVE}$ENV{HOMEPATH}\CMakeBuilds\...\build\x86-Debug\vcpkg-download
. Things are really meta at this point. cmake ExternalProject is used for that. Some tweaks in execute_process were necessary to pass the right${CMAKE_EXE_LINKER_FLAGS}
to build vcpkg with Visual Studio.
Observations
There are a couple of things vcpkg.cmake could make the experience better.- GIT_TAG ... simply did not work for me in
ExternalProject_Add
. Cloning a specific tag/branch/commit hash of vcpkg is important for reproducible builds. Btw, Why aren't there any official releases of vcpkg? There's not a single tag as of this writing. - The technique is this post is lower level but feels much more well-integrated. However, the end effect is the same. Not sure if it's worth the increased complexity. Especially because I had to overcome vcpkg build error "LINK : fatal error LNK1104: cannot open file 'MSVCRTD.lib'" that did not happen in part #1. The resulting Visual Studio project has some cruft too.
Comments