The right way to link with Gurobi using Modern CMake

Table of content

    Stop insulting your PC, you are in the right place. I know... Not being able to link with a given library is among the most awful feelings in computer programming. Fortunately, we are here to discuss this and to show how to link, in a proper way, with a C++ "library" called Gurobi.

    For a ready-to-use FindGUROBI.cmake, please visit my FindGUROBI.cmake repo.

    Gurobi is one of the most efficient mathematical optimization solver that exist in the world and is used by tons of people. Strangely enough, I have only seen little help on how to link with it using CMake. Even worse, the only examples I could found (e.g., searching for FindGUROBI.cmake on GitHub) are simply... wrong. What I mean by wrong is a little bit subjective, I have to admit. But I say this in the sense that, even though the examples you find may work, none of them are safe and none of them use the Modern CMake paradigm. This is, in particular true for the official solution given by Gurobi itself!

    For those of you who do not know what "Modern CMake" is. You can consider it as just a bunch of principles to be used when writting CMake files so that it is clean, elegant and powerful. It requires features that have been introduced in CMake versions 3.0.0 and more. The most important principles are as follows:

    • Treat CMake code like production code ;
    • Forget about add_compile_options, include_directories, link_directories, link_libraries ;
    • Be "target-oriented" instead.

    For more details on this, you may refer to the gist of mbinna or to the book Modern CMake for C++. To ease your task, I will start by showing you what we do not want (the ugly old-style CMake). Then, we will work on a better version. But, before that, we need to talk about how CMake finds new libraries in your system. Basically, what does find_package do?

    How does CMake finds packages

    All right, let's start with a new CMake project. I will assume that you already have a valid main.cpp. Let us start with the following standard CMakeLists.cmake.

    cmake_minimum_required(VERSION 3.19)
    project(my_project)
    
    add_executable(my_target main.cpp)
    

    What this code does is rather simple. First, the cmake_minimum_required command is called to specify CMake version requirements. I am using version 3.19 in this case (probably lower versions will work too). Then, a new project (i.e., a set of targets) is created with project. Then, a new target called my_target is created. This target will be an executable since the add_executable command is being used. Following the name my_target of the target, we have the list of source files which will be compiled for building the executable.

    Note that if you do not know what a target is, you can simply view it as a "CMake object" with correspoding attributes and methods. For instance, you may use set_target_properties to set your target's properties. Modern CMake asks for a target-oriented viewpoint while writting CMake files.

    OK, now let's search for our package! This is done by using the find_package command.

    find_package(GUROBI REQUIRED)

    The first parameter is the name of the package you are looking for, here, it is GUROBI. Then, REQUIRED tells CMake that the generation should stop if GUROBI is not found, since it is REQUIRED. Now let us try to run cmake on this. (Easy steps are to create a new folder mkdir build then to run cmake inside of it as follows: cd build && cmake ..). We get the following error message.

    CMake Error at CMakeLists.txt:14 (find_package):
      By not providing "FindGUROBI.cmake" in CMAKE_MODULE_PATH this project has
      asked CMake to find a package configuration file provided by "GUROBI", but
      CMake did not find one.
    
      Could not find a package configuration file provided by "GUROBI" with any
      of the following names:
    
        GUROBIConfig.cmake
        gurobi-config.cmake
    
      Add the installation prefix of "GUROBI" to CMAKE_PREFIX_PATH or set
      "GUROBI_DIR" to a directory containing one of the above files.  If "GUROBI"
      provides a separate development package or SDK, be sure it has been
      installed.
    

    Let us try to understand this message. First, CMake tells us that it was looking for a file called FindGUROBI.cmake but was unable to find it. It was looking for it in the CMAKE_MODULE_PATH directories. Note that you can print the list of paths by executing message("${CMAKE_MODULE_PATH}"). According to the documentation, this corresponds to the "Module" mode of find_package. Simply put, CMake searches for a file called FindGUROBI.cmake. If it cannot be found, an error is returned. If it is found, the file is executed and no error is returned. The library has been "found". From CMake's viewpoint, the hard part is to locate the file FindGUROBI.cmake since this file is supposed to do all the work to actually provide informations on how to link with GUROBI.

    Then, CMake switches to the "Config" mode (only when the "Module" mode failed). In this mode, CMake searches for files named GUROBIConfig.cmake or gurobi-config.cmake in the paths listed in CMAKE_PREFIX_PATH or in the GUROBI_DIR. In principle, these files should have been provided by the package developpers during the installation process. If this were the case, a simple execution of find_package(GUROBI REQUIRED) would have worked and be enough to "find" the package! Unfortunately, Gurobi does not provide such a file.

    Thus, we will have to "manually" tell CMake how to find GUROBI. Simply enough, we will do it by writing our own FindGUROBI.cmake file. Create a new folder called cmake with mkdir cmake and create a new file cmake/FindGUROBI.cmake. This is the file which we will be executed by CMake when calling find_package. Now, remember that CMake only looks for FindGUROBI.cmake in the list of directories which are in the CMAKE_MODULE_PATH variable. Thus, let us add cmake/ as a potential location folder. This is done as follows.

    set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")

    From now on, CMake should be able to find our file. Remember that CMake considers that finding the FindGUROBI.cmake file and executing it is enough to "actually" find the package. In our case, clearly, the FindGUROBI.cmake file is empty and does nothing. We will have to write it! Before diving into it, I would like to give you an example of what we do not want as a result. That is, the old-style-CMake way of doing things.

    The Ugly old-style CMake - What we do not want

    When searching the internet of examples FindGUROBI.cmake I have found a lot of files which could be used as follows.

    find_package(GUROBI REQUIRED)
    
    add_executable(my_target main.cpp)
    include_directories(${GUROBI_INCLUDE_DIRS})
    link_libraries(${GUROBI_LIBRARY})
    

    This type of code poses several issues. First, it relies on "global variables" which have been created by the execution of FindGUROBI.cmake. These variables are GUROBI_INCLUDE_DIRS and GUROBI_LIBRARY, for instance. These variables are globally accessible and non-const, meaning that they can be altered by any piece of CMake code by calling set(GUROBI_LIBRARY "my junk value"). What's more, to make these variables "globally accessible", the FindGUROBI.cmake (and therefore those who write this file) have to use the mark_as_advanced command. Just by the name, you can tell you should not be using it.

    Another issue is the use of include_directories and link_libraries which are non-target-based commands. In that sense, any given target will be linked with Gurobi and all targets will have access to the include directories. This is both unncessary and inelegant. Moreover, there is no control on how to propagate these dependencies. Consider creating a library which depends on other libraries, including Gurobi. How to tell CMake that a piece of code which links to your library should also link with Gurobi and other dependencies, and which dependencies should not be linked with?

    I think you get my point, let's write our own Modern CMake FindGUROBI.cmake file!

    Modern CMake

    OK, this part is dedicated to the actual writing of our FindGUROBI.cmake file. At the end of this section, you will be able to write the following CMake code.

    find_package(GUROBI REQUIRED)
    add_executable(my_target main.cpp)
    target_link_libraries(my_target PUBLIC gurobi)
    

    This piece of code is beautiful. And I am not (only) saying this because it is mine, but because it is target-oriented, global-variable-free and controls dependency propagation. It is easily read (only one call to target_link_libraries) and, even better, does not need an underlying FindGUROBI.cmake which make use of any strange behaviour such as mark_as_advanced commands. Let's start!

    At this point, I will assume that Gurobi is installed in your machine, following these official guidelines or that the corresponding variables (GUROBI_HOME, etc.) have been set accordingly. Once this is done, we can start writting our FindGUROBI.cmake file! Roughly speakking, we need to do the following steps:

    • Find the library files;
    • Find the include directory;
    • Check that everything has been found;
    • Create a target named gurobi and set its properties;

    Finding the library files

    According to this official Gurobi page, for our C++ code to work with Gurobi we need to link with two different libraries: the Gurobi C++ library libgurobi_c++.a and the Gurobi C library libgurobi95.so. Thus, we will have to look for both of these files. Let us start with libgurobi95.so. This is done as follows.

    find_library(
            GUROBI_LIBRARY
            NAMES gurobi gurobi81 gurobi90 gurobi95 gurobi100
            HINTS ${GUROBI_DIR} $ENV{GUROBI_HOME}
            PATH_SUFFIXES lib)
    

    Some explanations on this command is needed. First, find_library is broadly used to find the path of library files, e.g., .so or .a files, and stores the result in a cache variable. In our case, this variable's name is GUROBI_LIBRARY. Then, we give some indications on the possible names of the file which we are looking for with the NAMES keyword. Thus, we are telling CMake to search for a file with possible names gurobi, gurobi81, gurobi90 or gurobi95. Then, we also need to provide some "hints" on where to find this file by using the HINTS keyword. In our case, we are going to have a look to the directories path stored in the cache variable GUROBI_DIR or the path inside the environment variable GUROBI_HOME. If you installed Gurobi following the official guidelines, an appropriate environment variable should exists. Then, to each of these paths, we add the "lib" suffix by using the PATH_SUFFIXES keyword. Indeed, by default, we have the environment variable GUROBI_HOME=/opt/gurobi951/linux64 (or alike - again, following the official installation guidelines). Yet, the library file we are looking for is located inside /opt/gurobi951/linux64/lib. At this point, we should be able to run message("${GUROBI_LIBRARY}"). This should print out something like /opt/gurobi951/linux64/lib/libgurobi95.so.

    OK, now we need to do the same for the C++ library libgurobi_c++.a. The steps are very similar. For non-Visual-Studio-Code users, the following command will do the trick.

    find_library(
            GUROBI_CXX_LIBRARY
            NAMES gurobi_c++
            HINTS ${GUROBI_DIR} $ENV{GUROBI_HOME}
            PATH_SUFFIXES lib)
    

    Again, we are looking for a library file named gurobi_c++ inside the folders with path contained in the cache variable GUROBI_DIR or in the environment variable GUROBI_HOME with a suffix lib. For users of Visual Studio Code, I am relying on this "official Gurobi FindGUROBI.cmake file" which, as I anticipated, does not fulfill the Modern CMake standards and has the previously discussed drawbacks. All in all, we get the following code to search for libgurobi_c++.a.

    if(MSVC)
        # determine Visual Studio year
        if(MSVC_TOOLSET_VERSION EQUAL 142)
            set(MSVC_YEAR "2019")
        elseif(MSVC_TOOLSET_VERSION EQUAL 141)
            set(MSVC_YEAR "2017")
        elseif(MSVC_TOOLSET_VERSION EQUAL 140)
            set(MSVC_YEAR "2015")
        endif()
    
        if(MT)
            set(M_FLAG "mt")
        else()
            set(M_FLAG "md")
        endif()
    
        find_library(
                GUROBI_CXX_LIBRARY
                NAMES gurobi_c++${M_FLAG}${MSVC_YEAR}
                HINTS ${GUROBI_DIR} $ENV{GUROBI_HOME}
                PATH_SUFFIXES lib)
    else()
        find_library(
                GUROBI_CXX_LIBRARY
                NAMES gurobi_c++
                HINTS ${GUROBI_DIR} $ENV{GUROBI_HOME}
                PATH_SUFFIXES lib)
    endif()
    

    Finding the include directory

    Quite similarly, we search for the include directories by using the find_path command, which works much similarly to find_library. Have a look at the self-explained piece of code.

    find_path(
            GUROBI_INCLUDE_DIRS
            NAMES gurobi_c.h
            HINTS ${GUROBI_DIR} $ENV{GUROBI_HOME}
            PATH_SUFFIXES include)
    

    Checking that everything has been found

    We are now almost ready to create our gurobi target with which our own target should link. Yet, we first need to check that everything has been found. Among other things, this can be done thanks to the find_package_handle_standard_args command, which is defined in the FindPackageHandleStandardArgs module. It is used as follows.

    include(FindPackageHandleStandardArgs) # include the "FindPackageHandleStandardArgs" module
    find_package_handle_standard_args(GUROBI DEFAULT_MSG GUROBI_LIBRARY GUROBI_CXX_LIBRARY GUROBI_INCLUDE_DIRS)
    

    The find_package_handle_standard_args command will have two main effects. First, it will check that variables GUROBI_LIBRARY, GUROBI_CXX_LIBRARY and GUROBI_INCLUDE_DIRS have well been given a value. In other words, checks that we have found every path we needed. If this is the case, a variable GUROBI_FOUND is created and set to true. Then, its second effect is to take into account the REQUIRED or QUIET arguments of the find_package command. For instance, when we run find_package(gurobi REQUIRED), cmake will stop if the library cannot be found. All is clear, we are now ready to create our target!

    Creating the gurobi target

    If every path is found, we are good for creating our target. This target will be a "library" target, rather than an "executable" target. Thus, we will be using the add_library command, instead of the add_executable target. The C++ Gurobi library is a static library (its extension is .a) and we are going to "import" it. Thus, we do the following.

    add_library(gurobi STATIC IMPORTED)
    set_target_properties(gurobi PROPERTIES IMPORTED_LOCATION ${GUROBI_CXX_LIBRARY})
    

    Quite simply, we first create an imported static library as a target named gurobi, then set its IMPORTED_LOCATION property to ${GUROBI_CXX_LIBRARY}. Now, recall that any C++ program which is intended to work with gurobi should alos be linked with the C Gurobi library libgurobi95.so. We will therefore use the "dependency propagation" feature of CMake. First, we will link our gurobi target to the C library by using target_link_libraries command with the INTERFACE keyword. By choosing the INTERFACE keyword, CMake will automatically propagate this "linkage requirement" to any target linking with the gurobi imported target itself. The same is done for the include directories which we may add by using the target_include_directories command. This is done as follows.

    if (GUROBI_FOUND)
        add_library(gurobi STATIC IMPORTED)
        set_target_properties(gurobi PROPERTIES IMPORTED_LOCATION ${GUROBI_CXX_LIBRARY})
        target_link_libraries(gurobi INTERFACE ${GUROBI_LIBRARY})
        target_include_directories(gurobi INTERFACE ${GUROBI_INCLUDE_DIRS})
    endif()
    

    That's it! We are now done with writting our FindGUROBI.cmake file.

    FAQ: How do I resolve "undefined reference" errors while linking Gurobi in C++?

    This is a common issue. All you need to do is to re-compile the Gurobi library. Please, refer to this Official post from Gurobi.