After the last week's 2019 Microsoft MVP Summit, I decided to give Microsoft vcpkg a shot. I've a cmake project at work and we target Linux using the Hunter package manager. So vcpkg had been on the back-burner for me.
I'm targeting a4 part 3 part blog series.
If you prefer to clone/browse a github project. All contents in this blogpost are available under cpptruths/cpp0x/vcpkg_test (branch vcpkg_cmake_blog).
To start with, I've a barebones C++ project with nearly empty
There're many ways to structure the project. In this project I've chosen to use only one
I'm targeting a
- Bootstrapping a cmake project based on vcpkg in Visual Studio (this post)
- Bootstrapping a cmake project based on vcpkg in Linux and Visual Studio with idiomatic cmake (here)
- Bootstrapping a cmake project based on Hunter in Linux and Windows (here)
If you prefer to clone/browse a github project. All contents in this blogpost are available under cpptruths/cpp0x/vcpkg_test (branch vcpkg_cmake_blog).
To start with, I've a barebones C++ project with nearly empty
driver.cpp
and driver.h
files. Later, I'll add Boost core and optional as third party dependencies. Both are header-only. Later, we will add libraries requiring linking. So, let's get started.
A barebones C++ cmake project
The following is the project structure of my near-empty C++ projectvcpkg_test
vcpkg_test ├── CMakeLists.txt ├── include │ └── driver.h ├── src │ └── driver.cpp └── test └── driver_test.cpp 3 directories, 4 filesThe
driver.cpp
and driver_test.cpp
files have just a main function that does nothing. driver.h
is empty. The CMakeLists.txt
looks as follows.
cmake_minimum_required (VERSION 3.12) project (vcpkg_test CXX) set(CMAKE_CXX_STANDARD 17) add_executable(driver src/driver.cpp) target_include_directories(driver PUBLIC ${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)See the cmake tutorial if the above file is all greek. It builds two executables from the sources: driver and driver_test.
There're many ways to structure the project. In this project I've chosen to use only one
CMakeLists.txt
to build both the sources and the test. One could have added CMakeLists.txt in src and test sub-directories.
Open cmake Project in Visual Studio
Visual Studio 2017+ has built-in support for cmake projects. Yes, you read that right! You can open the folder containing the top-levelCMakeLists.txt
and Visual Studio will figure out everything. The loaded project looks very clean.
Things used to be very different not too long ago. cmake's native solution generator used to add additional targets that are not visible in the
CMakeLists.txt
you wrote. I always wondered what magic was going on there.
Visual Studio runs cmake automatically on the
CMakeLists.txt
.
Project build and rebuild works as expected.
Targets driver.exe
and driver_test.exe
are available in the drop-down. Here's how my loaded project looks like. No cruft!
So, that's how a toy C++ cmake project looks like. Let's use vcpkg to manage our third-party dependencies: boost-core and boost-optional.
Adding vcpkg to a cmake project
Here's a vcpkg tutorial to get your cmake project off the ground in Visual Studio. However, my goal is to create a reproducible build with maximum automation when a user clones the project directory. Perhaps something that could run as-is on AppVeyor CI servers. So the followingCMakeLists.txt
expects only Visual Studio 2017+ installed on a Windows machine.
The script clones the vcpkg repository and bootstraps it as necessary. We also change the
CMAKE_TOOLCHAIN_FILE
variable to point to the vcpkg instance the script downloaded and bootstrapped. This allows cmake to discover, include, and link packages managed by vcpkg. Here're the changes to CMakeLists.txt
.
cmake_minimum_required (VERSION 3.12) set(MY_PROJECT_DEPENDENCIES boost-core boost-optional boost-filesystem) if(NOT DEFINED ${CMAKE_TOOLCHAIN_FILE}) if(NOT DEFINED ENV{VCPKG_ROOT}) if(WIN32) set(VCPKG_ROOT $ENV{HOMEDRIVE}$ENV{HOMEPATH}/vcpkg_cpptruths) else() set(VCPKG_ROOT $ENV{HOME}/.vcpkg_cpptruths) endif() else() set(VCPKG_ROOT $ENV{VCPKG_ROOT}) endif() if(NOT EXISTS ${VCPKG_ROOT}) message("Cloning vcpkg in ${VCPKG_ROOT}") execute_process(COMMAND git clone https://github.com/Microsoft/vcpkg.git ${VCPKG_ROOT}) # If a reproducible build is desired (and potentially old libraries are # ok), uncomment the # following line and pin the vcpkg repository to a specific githash. # execute_process(COMMAND git checkout 745a0aea597771a580d0b0f4886ea1e3a94dbca6 WORKING_DIRECTORY ${VCPKG_ROOT}) else() # The following command has no effect if the vcpkg repository is in a detached head state. message("Auto-updating vcpkg in ${VCPKG_ROOT}") execute_process(COMMAND git pull WORKING_DIRECTORY ${VCPKG_ROOT}) endif() if(NOT EXISTS ${VCPKG_ROOT}/README.md) message(FATAL_ERROR "***** FATAL ERROR: Could not clone vcpkg *****") endif() if(WIN32) set(BOOST_INCLUDEDIR ${VCPKG_ROOT}/installed/x86-windows/include) set(VCPKG_EXEC ${VCPKG_ROOT}/vcpkg.exe) set(VCPKG_BOOTSTRAP ${VCPKG_ROOT}/bootstrap-vcpkg.bat) else() set(VCPKG_EXEC ${VCPKG_ROOT}/vcpkg) set(VCPKG_BOOTSTRAP ${VCPKG_ROOT}/bootstrap-vcpkg.sh) endif() if(NOT EXISTS ${VCPKG_EXEC}) message("Bootstrapping vcpkg in ${VCPKG_ROOT}") execute_process(COMMAND ${VCPKG_BOOTSTRAP} WORKING_DIRECTORY ${VCPKG_ROOT}) endif() if(NOT EXISTS ${VCPKG_EXEC}) message(FATAL_ERROR "***** FATAL ERROR: Could not bootstrap vcpkg *****") endif() set(CMAKE_TOOLCHAIN_FILE ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake CACHE STRING "") message(STATUS "***** Checking project third party dependencies in ${VCPKG_ROOT} *****") execute_process(COMMAND ${VCPKG_EXEC} install ${MY_PROJECT_DEPENDENCIES} WORKING_DIRECTORY ${VCPKG_ROOT}) endif()If everything goes well, the cmake script clones vcpkg repository under
$ENV{HOMEDRIVE}$ENV{HOMEPATH}/vcpkg_cpptruths
and bootstraps it (i.e., there're no pre-installed packages). From now on it will automatically use the CMAKE_TOOLCHAIN_FILE
from this directory. Of course, you can override the CMAKE_TOOLCHAIN_FILE
at the command prompt to point to a different vcpkg instance all different toolchain altogether. Also, feel free to change the path vcpkg_cpptruths to something you like.
Managing third-party dependencies with vcpkg
Now is the time to add the boost dependencies. Three steps are needed.- Write code that uses boost-core and boost-optional
- Instruct vcpkg to download and install boost-core and boost-optional
- Update
CMakeLists.txt
with the right dependencies
#include <iostream> #include <cstdlib> #include <ctime> #include <cmath> #include <typeinfo> #include "boost/core/demangle.hpp" #include "boost/filesystem.hpp" #include "driver.h" void check_exists(const char *filename) { using namespace boost::filesystem; path p(filename); if (exists(p)) { // does p actually exist? if (is_regular_file(p)) // is p a regular file? std::cout << p << " size is " << file_size(p) << '\n'; else if (is_directory(p)) // is p a directory? std::cout << p << " is a directory\n"; else std::cout << p << " exists, but is neither a regular file nor a directory\n"; } else std::cout << p << " does not exist\n"; } int main() { std::srand(static_cast<unsigned int>(std::time(0))); boost::optional<int> i = Generator::get_even_random_number(); if (i) { std::cout << std::sqrt(static_cast<float>(*i)) << "\n"; std::cout << boost::core::demangle(typeid(boost::optional<int>).name()) << "\n"; } check_exists("driver"); }For #2, you could open a shell and run
vcpkg install boost-core boost-optional boost-filesystem
. It's simple. However, I want a reproducible automatic build setup. So I'm going to have cmake run the same vcpkg command and install the dependencies it's going to use later.
set(MY_PROJECT_DEPENDENCIES boost-core boost-optional boost-filesystem) message(STATUS "***** Checking project third party dependencies in ${VCPKG_ROOT} *****") execute_process(COMMAND ${VCPKG_ROOT}/vcpkg.exe install ${MY_PROJECT_DEPENDENCIES} WORKING_DIRECTORY ${VCPKG_ROOT})The
execute_process
command gets the job done. However, I'm not sure, if there's a better to do the same thing. Take a look at part #2 with idiomatic cmake. Is there a higher-level cmake function(s) in vcpkg.cmake that would install the libraries in the vcpkg instance (pointed by the CMAKE_TOOLCHAIN_FILE
).
Saving the file
CMakeLists.txt
in Visual Studio runs it and installs the packages in ${MY_PROJECT_DEPENDENCIES}
.
Now we update CMakeLists.txt
to look for boost libraries. This part step is platform and package-manger independent.
find_package(Boost 1.67 REQUIRED COMPONENTS filesystem) add_executable(driver src/driver.cpp) target_include_directories(driver PUBLIC ${Boost_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/include) target_link_libraries(driver ${Boost_LIBRARIES})
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_DIR
, Boost_LIBRARIES
are set by find_package
(only upon success).
You may have to regenerate the cmake cache as the
CMAKE_TOOLCHAIN_FILE
has been updated. You can do that by right-clicking on CMakeLists.txt
.
At this point the code builds and runs cleanly for me. No squiggles.
Observations
Some things I noted would make the experience nicer in Visual Studio 2019.- The Open Project/Solution dialog box did not show
CMakeLists.txt
under "All Project Files" drop down. First-class support should make the experience seamless. - If vcpkg is integrated with Visual Studio such that libraries get installed in the right vcpkg instance, that would be great.
- It would be nice to have cmake functions in vcpkg.cmake that would install libraries in the vcpkg instance. I received responses from multiple people who had some ground work here.
- See Package Manager Manager (pmm) mentioned on reddit/r/cpp.
- Google-cloud-cpp/super project uses cmake functionality such as ExternalProject_Add and other friends to bootstrap a vcpkg instance.
- After updating
CMakeLists.txt
, the output of cmake is not displayed in the IDE right-away. It takes a good minute and it appears like Visual Studio is stuck. Seems like cmake does not flush output to the IDE window right-away.
Comments