Cross-Platform Configurations
Learn the tools and patterns needed to write portable CMakeLists.txt files that work seamlessly across platforms and compilers
One of the goals of our build configuration is to have it be portable. The dream is to write one CMakeLists.txt
file, and have any developer on any machine - Windows, macOS, or Linux- be able to build the project with a few simple commands.
In the , we took a major step toward this by using generator expressions, which let us handle different build configurations in a portable way.
Now, we'll tackle the next layer of complexity: handling differences between operating systems and compilers. C++ code is often portable, but the environment it builds and runs in is not. File paths, compiler flags, and system libraries all have platform-specific quirks.
This lesson will cover the main tools to write a universal CMakeLists.txt
. We'll learn how to detect the host system and compiler, and how to use that information to conditionally include source files, set compiler flags, and link against the correct libraries for the target platform.
Working With Paths
A path written for one OS will break on another:
# Bad: Windows-specific backslashes
set(MY_SOURCES "src\\main.cpp")
Instead, always use forward slashes (/
) for paths. CMake will automatically convert them to the native format (\\
) on Windows.
# Good: Use / by default
set(MY_SOURCES "src/main.cpp")
More importantly, using exact absolute path to where things are on your hard drive will not work for people storing the project in a different location:
target_include_directories(MyTarget PUBLIC
# Bad: Absolute path
"/usr/greg/my-project/include"
)
Instead, always construct paths that are relative to the project structure. The CMake variables we covered earlier in the course, such as PROJECT_SOURCE_DIR
, can help with this:
target_include_directories(MyTarget PUBLIC
# Good: Relative Path
"${PROJECT_SOURCE_DIR}/include"
)
Detecting the Operating System
CMake automatically defines a set of boolean variables that tell you which platform you're configuring for. These variables allow you to create conditional blocks that execute only on a specific OS. The three most important are:
WIN32
: True on all versions of Windows, including 64-bit.APPLE
: True on all Apple operating systems, including macOS and iOS.UNIX
: True on all UNIX-like systems, which notably includes both Linux and macOS (APPLE
systems are alsoUNIX
).
The most common pattern is to check for Windows first, and then treat everything else as Unix-like.
Example: Platform-Specific Source Files
Let's expand on an example from a previous lesson. Imagine our GreeterLib
needs to display the current operating system, which requires calling a platform-specific API.
First, we'll define a common interface in our public header.
greeter/include/greeter/Greeter.h
#pragma once
#include <string>
std::string get_greeting();
std::string get_os_string(); // New function
Next, we'll provide two different implementations for this function.
Files
Finally, in greeter/CMakeLists.txt
, we use an if()
block to choose which source file to compile into our library.
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
set(LIB_SOURCES src/Greeter.cpp)
if(WIN32)
list(APPEND LIB_SOURCES src/platform_win.cpp)
else() # We assume anything not Windows is Unix-like
list(APPEND LIB_SOURCES src/platform_nix.cpp)
endif()
add_library(GreeterLib ${LIB_SOURCES})
target_include_directories(GreeterLib PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
Now, the GreeterLib
target will be built with the correct implementation file for the host OS, while the interface (Greeter.h
) remains consistent for all consumers. We no longer need to perform this platform check in our code - out build system is taking care of it:
app/src/main.cpp
#include <iostream>
#include <greeter/Greeter.h>
int main() {
std::cout << get_greeting();
std::cout << '\n' << get_os_string();
return 0;
}
Hello from the modular greeter library!
This is Windows!
Abstract Away Platform Details
In addition to removing platform-specific logic from our code, a good goal is to minimize the amount of platform-specific logic even in our build scripts.
For example, the following approach duplicates platform logic to link SomeLibrary
to targets, but only on Windows:
# App1 needs a windows-specific library
if(WIN32)
target_link_libraries(App1 PRIVATE SomeLibrary)
endif()
# Bad: App2 duplicates the same logic
if(WIN32)
target_link_libraries(App2 PRIVATE SomeLibrary)
endif()
Instead of scattering WIN32
checks everywhere, you might create an INTERFACE
target that encapsulates the platform-specific detail:
# Create an INTERFACE target to represent platform dependencies
add_library(PlatformLibs INTERFACE)
# In one place, add the windows library to the interface
if(WIN32)
target_link_libraries(PlatformLibs INTERFACE SomeWindowsLibrary)
endif()
Consumers now link to the abstraction - no further Win32
checks needed:
target_link_libraries(App1 PRIVATE PlatformLibs)
target_link_libraries(App2 PRIVATE PlatformLibs)
Example: Linking System Libraries
The most common cross-platform challenge is linking against system libraries, which often have different names or don't exist at all on other platforms.
For example, Windows applications that use networking sockets often need to link against the ws2_32.lib
library. On Linux and macOS, this functionality is part of the standard library and doesn't require an explicit link.
Let's create a Networking
library that implements this platform-specific dependency.
networking/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_library(Networking INTERFACE)
# On Windows, we need to link the sockets library.
if(WIN32)
target_link_libraries(Networking INTERFACE ws2_32)
endif()
In our root CMakeLists.txt
, we can add this new subdirectory.
CMakeLists.txt
add_subdirectory(greeter)
add_subdirectory(app)
add_subdirectory(networking)
Now, any target in our project that needs networking can simply link against our abstract Networking
target:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
target_link_libraries(GreeterApp Networking)
Again, note that the consumer - GreeterApp
in this case - does not need to perform any platform checks here. It doesn't need to know why or how networking works; it just needs to state its requires networking support, expressed by linking to the Networking
interface.
If this project is configured on Windows, the Networking
target will carry the ws2_32
link dependency with it, and SomeOtherTarget
will be correctly linked. If configured on Linux, the Networking
target will have no link dependencies, and nothing extra will be added to the link line.
Detecting the Compiler
Just as with the operating system, CMake detects the compiler being used and sets several variables accordingly. This is helpful for setting compiler-specific flags or working around compiler-specific bugs.
The most common variables for checking the compiler family are:
MSVC
: True if the compiler is Microsoft Visual C++ (or a compatible front-end likeclang-cl
).GNU
: True for the GNU Compiler Collection (GCC).Clang
: True for the Clang compiler.
Example: Compiler-Specific Warning Flags
In a previous lesson, we created an INTERFACE
library called StrictWarnings
to apply a set of warning flags. However, the flags we used (-Wall
, -Wextra
) were specific to GCC and Clang. They will cause an error if used with MSVC.
We can make our StrictWarnings
target portable by detecting the compiler and setting the appropriate flags for each one.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
add_library(StrictWarnings INTERFACE)
if(MSVC)
# Use Microsoft-specific warning level 4
target_compile_options(StrictWarnings INTERFACE /W4)
else()
# Assume GCC/Clang-compatible flags for others
target_compile_options(StrictWarnings INTERFACE
-Wall
-Wextra
-Wpedantic
)
endif()
add_subdirectory(greeter)
add_subdirectory(app)
Now, our StrictWarnings
target is a portable abstraction. Any internal target can link against it and receive the correct, most stringent warning level for whatever compiler is being used, without the target needing to know any of the compiler-specific details.
target_link_libraries(MyTarget PRIVATE StrictWarnings)
Summary
Writing portable CMake scripts is about abstracting away platform differences. By using CMake's introspection variables, you can create a CMakeLists.txt
that correctly assembles the right source files, compiler flags, and library links for any environment.
- Stay Portable: Avoid hardcoded paths and platform-specific commands. Use CMake's variables (
${PROJECT_SOURCE_DIR}
) and commands (target_link_libraries
) to express your intent. - Detect the OS: Use
if(WIN32)
,if(APPLE)
, andif(UNIX)
to create conditional blocks for OS-specific logic. - Detect the Compiler: Use
if(MSVC)
,if(GNU)
, andif(Clang)
to set compiler-specific flags or work around compiler differences. - Abstract Dependencies: When dealing with platform-specific libraries or source files, create an
INTERFACE
target to represent the abstract dependency. Consumers can then link to this portable target, and the platform-specific details will be handled automatically.