Skip to main content

Bootstrapping a vcpkg-based cmake project in Visual Studio

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 a 4 part 3 part blog series.
  1. Bootstrapping a cmake project based on vcpkg in Visual Studio (this post)
  2. Bootstrapping a cmake project based on vcpkg in Linux and Visual Studio with idiomatic cmake (here)
  3. Bootstrapping a cmake project based on Hunter in Linux and Windows (here)
As of this writing I'm new to vcpkg. So I apologize in advance if you are annoyed by noob mistakes. Please leave a comment if you notice something.

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++ project vcpkg_test
vcpkg_test
├── CMakeLists.txt
├── include
│   └── driver.h
├── src
│   └── driver.cpp
└── test
    └── driver_test.cpp

3 directories, 4 files
The 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-level CMakeLists.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 following CMakeLists.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.
  1. Write code that uses boost-core and boost-optional
  2. Instruct vcpkg to download and install boost-core and boost-optional
  3. Update CMakeLists.txt with the right dependencies
Here's my test code that uses boost-core and boost-optional.
#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.
  1. 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.
  2. If vcpkg is integrated with Visual Studio such that libraries get installed in the right vcpkg instance, that would be great.
  3. 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.
    1. See Package Manager Manager (pmm) mentioned on reddit/r/cpp.
    2. Google-cloud-cpp/super project uses cmake functionality such as ExternalProject_Add and other friends to bootstrap a vcpkg instance.
  4. 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

Popular Content

Unit Testing C++ Templates and Mock Injection Using Traits

Unit testing your template code comes up from time to time. (You test your templates, right?) Some templates are easy to test. No others. Sometimes it's not clear how to about injecting mock code into the template code that's under test. I've seen several reasons why code injection becomes challenging. Here I've outlined some examples below with roughly increasing code injection difficulty. Template accepts a type argument and an object of the same type by reference in constructor Template accepts a type argument. Makes a copy of the constructor argument or simply does not take one Template accepts a type argument and instantiates multiple interrelated templates without virtual functions Lets start with the easy ones. Template accepts a type argument and an object of the same type by reference in constructor This one appears straight-forward because the unit test simply instantiates the template under test with a mock type. Some assertion might be tested in...

Covariance and Contravariance in C++ Standard Library

Covariance and Contravariance are concepts that come up often as you go deeper into generic programming. While designing a language that supports parametric polymorphism (e.g., templates in C++, generics in Java, C#), the language designer has a choice between Invariance, Covariance, and Contravariance when dealing with generic types. C++'s choice is "invariance". Let's look at an example. struct Vehicle {}; struct Car : Vehicle {}; std::vector<Vehicle *> vehicles; std::vector<Car *> cars; vehicles = cars; // Does not compile The above program does not compile because C++ templates are invariant. Of course, each time a C++ template is instantiated, the compiler creates a brand new type that uniquely represents that instantiation. Any other type to the same template creates another unique type that has nothing to do with the earlier one. Any two unrelated user-defined types in C++ can't be assigned to each-other by default. You have to provide a...

Multi-dimensional arrays in C++11

What new can be said about multi-dimensional arrays in C++? As it turns out, quite a bit! With the advent of C++11, we get new standard library class std::array. We also get new language features, such as template aliases and variadic templates. So I'll talk about interesting ways in which they come together. It all started with a simple question of how to define a multi-dimensional std::array. It is a great example of deceptively simple things. Are the following the two arrays identical except that one is native and the other one is std::array? int native[3][4]; std::array<std::array<int, 3>, 4> arr; No! They are not. In fact, arr is more like an int[4][3]. Note the difference in the array subscripts. The native array is an array of 3 elements where every element is itself an array of 4 integers. 3 rows and 4 columns. If you want a std::array with the same layout, what you really need is: std::array<std::array<int, 4>, 3> arr; That's quite annoying for...