Search code examples
cmakeinclude-pathcompiler-flagslinker-flagscmake-presets

How should I conditionally set -isystem and -L flags in CMake?


I have a project that is to be compiled for Linux and Windows. Compilation for the former is done in a Linux environment, and compilation for the latter is done in w64devkit, i.e. I don't need to consider cross-compiling from a single host.

For the Windows build, all compiled files need the -isystem $(VCPKG_DIR)/installed/x64-windows/include compile flag, and the -L $(VCPKG_DIR)/installed/x64-windows/lib linker flag, where $(VCPKG_DIR) refers to the base location where vcpkg is installed.

(For simplicity, the rest of this question focuses on the include path)

This seemed like a good application for CMake presets, something along the lines of:

{
  "version": 1,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 19,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "linux",
      "displayName": "Build for Linux",
      "description": "Linux build using make generator",
      "generator": "Unix Makefiles",
      "binaryDir": "${sourceDir}/build"
    },
    {
      "name": "win64",
      "displayName": "Build for Windows",
      "description": "Windows build using make generator",
      "generator": "Unix Makefiles",
      "binaryDir": "${sourceDir}/build",
      "CMAKE_C_FLAGS": "${VCPKG_DIR}/installed/x64-windows/include"
    }
  ]
}

But from this answer, I learned that the only way we should set include paths are via target_include_directories() (or target_sources()).

This makes sense, but my concern is that in my project, I have many directories (that are peers of each other), each of which has CMakeLists.txt. It seems, then, that I would need to add a target_include_directories() line in each of those CMakeLists.txt files, e.g.

target_include_directories(submodule SYSTEM ${VCPKG_DIR}/installed/x64-windows/include)

I.e. my concern is the repetition of this line across multiple CMakeLists.txt files. This seems like a good candidate for consolidation at one location, something to the effect "if building for Windows, always add -I ${VCPKG_DIR}/installed/x64-windows/include", which seems kind of like the intent of CMake Presets.

But if this is not achievable, or not best practice, with CMake Presets, then how can I make adding that include path in each CMakeLists.txt file conditional on the build target? Would I have to do something like:

if(WIN64)
    target_include_directories(my_executable SYSTEM "${VCPKG_DIR}/installed/x64-windows/include")
endif()

?

My question is: what is the most consolidated (least repetitive) way, or the idiomatically-correct way of conditionalizing an include-path across many CMakeLists.txt that is conditional on the build target? Put another way: if my logic is "when building for win64, always include ${VCPKG_DIR}/installed/x64-windows/include as a system include path," what is the best (or idiomatically correct) way to implement that in CMake?


Solution

  • Here's a minimal example for you. I did it on Linux, but the steps should be identical on Windows. I assume CMake is up to date and you have Ninja installed.

    First we install vcpkg and libuv. This took about 5 minutes on my laptop.

    $ cd dev
    ~/dev$ git clone https://github.com/microsoft/vcpkg.git
    ~/dev$ cd vcpkg
    ~/dev/vcpkg$ ./bootstrap.sh -disableMetrics
    ~/dev/vcpkg$ vcpkg install libuv
    

    Now we create an example project:

    ~/dev/vcpkg$ cd ~/dev
    ~/dev$ mkdir example
    ~/dev$ cd example
    ~/dev/example$ touch CMakeLists.txt main.cpp
    

    Now here's the contents of CMakeLists.txt:

    cmake_minimum_required(VERSION 3.28)
    project(example)
    
    find_package(libuv REQUIRED)
    
    add_executable(main main.cpp)
    target_link_libraries(
      main PRIVATE $<IF:$<TARGET_EXISTS:libuv::uv_a>,libuv::uv_a,libuv::uv>
    )
    

    And this is main.cpp. Just enough to test that we can find the header.

    #include <uv.h>
    int main () { return 0; }
    

    Now to build this:

    $ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=$HOME/dev/vcpkg/scripts/buildsystems/vcpkg.cmake
    -- The C compiler identification is GNU 11.4.0
    -- The CXX compiler identification is GNU 11.4.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done (25.2s)
    -- Generating done (0.0s)
    -- Build files have been written to: /home/alex/dev/example/build
    $ cmake --build build --verbose
    Change Dir: '/home/alex/dev/example/build'
    
    Run Build Command(s): /usr/bin/ninja -v
    [1/2] /usr/bin/c++  -isystem /home/alex/dev/vcpkg/installed/x64-linux/include -g -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /home/alex/dev/example/main.cpp
    [2/2] : && /usr/bin/c++ -g  CMakeFiles/main.dir/main.cpp.o -o main  /home/alex/dev/vcpkg/installed/x64-linux/debug/lib/libuv.a  -lpthread  -ldl  -lrt && :
    

    In the output here I can very clearly see -isystem .../vcpkg/installed/x64-linux/include.

    This works because linking to targets (i.e. either libuv::uv or libuv::uv_a) automatically triggers propagation of the library's public include path(s) (i.e. vcpkg/installed/x64-linux/include) to your target. Because this target was imported via find_package, CMake knows to use -isystem rather than -I. It also uses the full path to the static library rather than risking name resolution issues with -L. This is the better behavior anyway.


    This fragment is a bit of a hack:

    $<IF:$<TARGET_EXISTS:libuv::uv_a>,libuv::uv_a,libuv::uv>
    

    CMake lacks a standardized way of selecting between static and shared libraries, so every project seems to do their own thing. Libuv happens to name their shared library libuv::uv and their static library libuv::uv_a. Vcpkg will only ever have one available, though.

    The above expression will resolve to libuv::uv_a if it exists and to libuv::uv otherwise.


    I strongly suggest you read the documentation on using third-party libraries in CMake: https://cmake.org/cmake/help/latest/guide/using-dependencies/index.html

    I would also peruse this documentation, especially the part on transitive usage requirements: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html