VS: Custom MSVC toolchain investigation
We have a custom MSVC toolchain. However, we were getting an issue related to RC files. That only manifested on Ninja dirty builds.
# Here is how the bug will manifest itself:
# "ninja: error: FindFirstFileExA(note: including file: d:/foobar/ms_sdk/n20246/10/include/10.0.20246.0/shared): The filename, directory name, or volume label syntax is incorrect."
I've posted about the issue here on the discourse:
cmake_minimum_required(VERSION 3.15)
set(DEVELOPMENT_KIT_ROOT "C:/foobar")
set(FOOBAR_MSVC_VERSION "14.24.28314")
set(FOOBAR_WINDOWS_VERSION "19041")
set(FOOBAR_ARCH "x64")
set(FOOBAR_TOOLSET "x64")
list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES
DEVELOPMENT_KIT_ROOT
FOOBAR_MSVC_VERSION
FOOBAR_WINDOWS_VERSION
FOOBAR_ARCH
FOOBAR_TOOLSET
)
# Define the target system and version:
# Only set CMAKE_SYSTEM_NAME when it's different than the host system name.
# Otherwise CMAKE_CROSSCOMPILING will be set to true unneccessarily.
# Which can cause longer builds for developers in some situations.
# https://discourse.cmake.org/t/do-i-need-to-set-the-cmake-system-name-when-making-a-toolchain-file/2861
if (NOT (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") )
set(CMAKE_SYSTEM_NAME "Windows" CACHE INTERNAL "")
endif()
set(CMAKE_SYSTEM_VERSION "10.0.${FOOBAR_WINDOWS_VERSION}.0" CACHE INTERNAL "")
if(FOOBAR_ARCH STREQUAL "x64")
set(CMAKE_SYSTEM_PROCESSOR AMD64)
elseif(FOOBAR_ARCH STREQUAL "x86")
set(CMAKE_SYSTEM_PROCESSOR x86)
else()
message(FATAL_ERROR "The only supported target architectures are x86 and x64!")
endif()
# Error check toolset
if (FOOBAR_TOOLSET STREQUAL "x64")
elseif (FOOBAR_TOOLSET STREQUAL "x86")
else()
message(FATAL_ERROR "The only supported toolsets are x64 and x86!")
endif()
# Setup FOOBAR DK directories
set(FOOBAR_MSVC_ROOT ${DEVELOPMENT_KIT_ROOT}/vc/${FOOBAR_MSVC_VERSION})
set(FOOBAR_MSVC_BIN_TOOLSET ${FOOBAR_MSVC_ROOT}/bin/Host${FOOBAR_TOOLSET}/${FOOBAR_ARCH})
set(FOOBAR_SDK_ROOT ${DEVELOPMENT_KIT_ROOT}/ms_sdk/n${FOOBAR_WINDOWS_VERSION}/10)
set(FOOBAR_SDK_BIN_TOOLSET ${FOOBAR_SDK_ROOT}/bin/10.0.${FOOBAR_WINDOWS_VERSION}.0/${FOOBAR_TOOLSET})
set(FOOBAR_SDK_LIB ${FOOBAR_SDK_ROOT}/lib/10.0.${FOOBAR_WINDOWS_VERSION}.0)
set(FOOBAR_SDK_INC ${FOOBAR_SDK_ROOT}/include/10.0.${FOOBAR_WINDOWS_VERSION}.0)
set(FOOBAR_WDK_ROOT ${DEVELOPMENT_KIT_ROOT}/ms_wdk/n${FOOBAR_WINDOWS_VERSION})
set(FOOBAR_WDK_LIB ${FOOBAR_WDK_ROOT}/lib/10.0.${FOOBAR_WINDOWS_VERSION}.0)
set(FOOBAR_WDK_INC ${FOOBAR_WDK_ROOT}/include/10.0.${FOOBAR_WINDOWS_VERSION}.0)
# Error checking
# I'm checking if the DEVELOPMENT_KIT_ROOT is defined as an absolute path
# because IS_DIRECTORY is only well defined if the path is absolute
# https://cmake.org/cmake/help/latest/command/if.html?highlight=is_directory#if
if ( IS_ABSOLUTE ${DEVELOPMENT_KIT_ROOT} )
set(FOOBAR_DK_DIRECTORIES_TO_ERROR_CHECK
${FOOBAR_MSVC_ROOT} ${FOOBAR_SDK_ROOT} ${FOOBAR_SDK_LIB}
${FOOBAR_SDK_INC} ${FOOBAR_WDK_ROOT} ${FOOBAR_WDK_LIB} ${FOOBAR_WDK_INC}
${FOOBAR_SDK_BIN_TOOLSET} ${FOOBAR_MSVC_BIN_TOOLSET}
)
# Make sure each directory exists
foreach(dir IN LISTS FOOBAR_DK_DIRECTORIES_TO_ERROR_CHECK)
if (NOT IS_DIRECTORY ${dir})
message(FATAL_ERROR "Include directory is not valid: ${dir}\n"
"Make sure to check your perforce mappings!\n")
endif()
endforeach()
endif()
# Cross Compiler
# In order to pass Visual Studio compiler identification we have to establish these variables
foreach(LANG C CXX)
set(CMAKE_${LANG}_COMPILER "${FOOBAR_MSVC_BIN_TOOLSET}/cl.exe" CACHE FILEPATH "")
set(CMAKE_${LANG}_COMPILER_ID "MSVC" CACHE STRING "")
# Extract the compiler version
execute_process(
# Execute cl.exe on the command line to extract the compiler version
COMMAND ${CMAKE_${LANG}_COMPILER}
# cl.exe outputs some of its output to standard error
# EX:
# Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27032.1 for x64
# Copyright (C) Microsoft Corporation. All rights reserved.
ERROR_VARIABLE cl_exe_ouput
# cl.exe outputs some of its output to standard output
# So just ignore this it isn't important
# EX:
# usage: cl [ option... ] filename... [ /link linkoption... ]
OUTPUT_QUIET
)
# Regex for the compiler version
string(REGEX MATCH ".* Version ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\).*" out ${cl_exe_ouput})
# If the regex fails then fatal error
if (NOT (${CMAKE_MATCH_COUNT} EQUAL "1"))
message(FATAL_ERROR "MSVC version regex failed")
endif()
# You have to set the compiler version
set(CMAKE_${LANG}_COMPILER_VERSION "${CMAKE_MATCH_1}" CACHE STRING "")
endforeach()
# This variable is neccessary to set for MSVC compilers greater than version 19.0 and greater
#
# Here is my attempt at an explanation (since there is no documentation online):
# VS2015 update 3 and above support language standard level flags, with the default and minimum level
# being C++14. This allows cmake to set variables/compiler-flags much more appropriately.
#
# To see the exact details see this module in your cmake implemenation.
# ...\share\cmake-3.16\Modules\Compiler\MSVC-CXX.cmake
#
# If that still doesn't make sense remove this check/set below
#
# To be honest although I know this logic below fixes the problem and is correct.
# It is still pretty hacky, and is a good argument to not use the DK in the future.
if (${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER "19.0")
set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "14" CACHE INTERNAL "")
endif()
# Cross Tools
set(CMAKE_RC_COMPILER "${FOOBAR_SDK_BIN_TOOLSET}/rc.exe" CACHE FILEPATH "")
set(CMAKE_MT "${FOOBAR_SDK_BIN_TOOLSET}/mt.exe" CACHE FILEPATH "")
# Generator Tools
if (CMAKE_GENERATOR MATCHES "Visual Studio")
# MSVC_IDE doesn't seem to work in toolchains?
# https://discourse.cmake.org/t/cannot-use-msvc-ide-in-toolchain/2734
set(FOOBAR_MSVC_IDE ON)
endif()
if (FOOBAR_MSVC_IDE)
# Build with Multiple Processes, this is a nice quality of life for VS users
# This is the preferred way of speeding up vs builds.
# Also /MP option enables /FS by default.
set(genFlag "/MP")
else()
# If multiple CL.EXE write to the same .PDB file, please use /FS
# This ensures multi-threads builds like Ninja work properly
set(genFlag "/FS")
endif()
# Set the flags all projects will be forced to use.
# https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS_INIT.html
foreach(LANG C CXX)
set(CMAKE_${LANG}_FLAGS_INIT "${genFlag}")
endforeach()
set(FOOBAR_DK_INCLUDE_DIRECTORIES
"${FOOBAR_MSVC_ROOT}/include"
"${FOOBAR_SDK_INC}/ucrt"
"${FOOBAR_SDK_INC}/um"
"${FOOBAR_SDK_INC}/shared"
"${FOOBAR_WDK_INC}/km"
"${FOOBAR_WDK_INC}/um"
"${FOOBAR_WDK_INC}/shared"
)
set(FOOBAR_DK_LIBRARY_DIRECTORIES
"${FOOBAR_MSVC_ROOT}/lib/${FOOBAR_ARCH}"
"${FOOBAR_MSVC_ROOT}/atlmfc/lib/${FOOBAR_ARCH}"
"${FOOBAR_SDK_LIB}/ucrt/${FOOBAR_ARCH}"
"${FOOBAR_SDK_LIB}/um/${FOOBAR_ARCH}"
"${FOOBAR_WDK_LIB}/km/${FOOBAR_ARCH}"
"${FOOBAR_WDK_LIB}/um/${FOOBAR_ARCH}"
)
if (FOOBAR_MSVC_IDE)
string(REGEX MATCHALL "Visual Studio [0-9]+ ([0-9]+)" vsVersions ${CMAKE_GENERATOR})
set(vsYear ${CMAKE_MATCH_1})
if (${vsYear} VERSION_LESS "2010")
message(FATAL_ERROR "CMAKE_VS_SDK_*_DIRECTORIES require at least Visual Studio 2010")
endif()
# Set the appropriate binary directories
set(CMAKE_VS_SDK_EXECUTABLE_DIRECTORIES "")
list(APPEND CMAKE_VS_SDK_EXECUTABLE_DIRECTORIES ${FOOBAR_MSVC_BIN_TOOLSET})
list(APPEND CMAKE_VS_SDK_EXECUTABLE_DIRECTORIES ${FOOBAR_SDK_BIN_TOOLSET})
# Don't allow MSVC defaults
set(CMAKE_VS_SDK_SOURCE_DIRECTORIES "")
set(CMAKE_VS_SDK_LIBRARY_WINRT_DIRECTORIES "")
set(CMAKE_VS_SDK_REFERENCE_DIRECTORIES "")
set(CMAKE_VS_SDK_EXCLUDE_DIRECTORIES "")
# Set include/library directories
set(CMAKE_VS_SDK_LIBRARY_DIRECTORIES ${FOOBAR_DK_LIBRARY_DIRECTORIES})
set(CMAKE_VS_SDK_INCLUDE_DIRECTORIES ${FOOBAR_DK_INCLUDE_DIRECTORIES})
# Platform Selection
# https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2016%202019.html#platform-selection
if ( ${vsYear} VERSION_GREATER_EQUAL "2019")
if(FOOBAR_ARCH STREQUAL "x64")
set(CMAKE_GENERATOR_PLATFORM "x64")
elseif(FOOBAR_ARCH STREQUAL "x86")
set(CMAKE_GENERATOR_PLATFORM "Win32")
else()
message(FATAL_ERROR "Unsupported platform ${FOOBAR_ARCH}")
endif()
endif()
# Toolset Selection
# https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2016%202019.html#toolset-selection
# host=<arch> specifies the host tools architecture as x64 or x86
#
# This is basically a quality of life to avoid cluttering the command line
set(CMAKE_GENERATOR_TOOLSET "host=${FOOBAR_TOOLSET}")
# Work Around:
# This command is only meaningful in the sense that it prevents Cmake from
# choosing a default toolset. 'FOOBAR_DK_TOOLSET' has no instrinsic value
# If you get rid of this, Visual Studio will still build correctly
# However, configuration will give weird output.
# It will list the default MSVC compiler instead of the one we chose.
set(CMAKE_VS_PLATFORM_TOOLSET_VERSION "FOOBAR_DK_TOOLSET")
else()
# Link Directories
foreach(TARGET EXE SHARED STATIC MODULE)
# W/A: CMake doesn't define CMAKE_<LANG>_STANDARD_LINK_DIRECTORIES.
# https://gitlab.kitware.com/cmake/cmake/issues/18222
set(CMAKE_${TARGET}_LINKER_FLAGS_INIT "")
foreach(dir IN LISTS FOOBAR_DK_LIBRARY_DIRECTORIES)
list(APPEND CMAKE_${TARGET}_LINKER_FLAGS_INIT
"/LIBPATH:${dir}"
)
endforeach()
# Neccessary for linker syntax
string(REPLACE ";" " " CMAKE_${TARGET}_LINKER_FLAGS_INIT "${CMAKE_${TARGET}_LINKER_FLAGS_INIT}")
endforeach()
# Include Directories
# NOTE: C/CXX make sense. The tricky one is RC. This is neccessary to support resource files.
foreach(LANG C CXX RC)
set(CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES ${FOOBAR_DK_INCLUDE_DIRECTORIES})
endforeach()
# This is neccessary to ensure RC files compile with our custom toolchain
# Otherwise dirty builds will break. This code basically patches a cmake bug.
#
# Here is the bug will manifest itself:
# "ninja: error: FindFirstFileExA(note: including file: d:/p4/DEVELOPMENT_KIT_ROOT/win/ms_sdk/n20246/10/include/10.0.20246.0/shared): The filename, directory name, or volume label syntax is incorrect."
if (CMAKE_GENERATOR MATCHES "Ninja")
# Compiling RC files needs to set this. The Ninja generator calls cmcldeps.exe to extract dependencies for RC
# files. But it configurates DepType to "gcc" so cmake cannot filter show includes prefix and the dependencies
# are stored in .ninja_deps with the prefix and cause next run of ninja to fail. The DepType cannot be forced
# to "msvc" otherwise the dependency changes won't trigger recompilation of RC files.
# The workaround is to set CMAKE_CL_SHOWINCLUDES_PREFIX to the exact English string "Note: including file: ".
# function CMAKE_DETERMINE_MSVC_SHOWINCLUDES_PREFIX may be used if any locale issue occurs.
set(CMAKE_CL_SHOWINCLUDES_PREFIX "Note: including file: " CACHE INTERNAL "FOOBAR WORKAROUND")
# The Ninja generator relies on the following variables to determine MSVC style PDB names. But these variables
# stored in cmake are only initialized to "" when it is first time run on a project before CMake${lang}Compiler
# .cmake are generated. The next run doesn't initilazed them because CMake${lang}Compiler.cmake are there. So
# they are null and SetMsvcTargetPdbVariable() returns false so ".dbg" suffix is used for PDBs.
# The workaround is to set them to either x64 or x86 (actually "" is enough).
#
# https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ARCHITECTURE_ID.html?highlight=architecture_id
set(CMAKE_CXX_ARCHITECTURE_ID ${FOOBAR_ARCH} CACHE INTERNAL "FOOBAR WORKAROUND")
set(CMAKE_C_ARCHITECTURE_ID ${FOOBAR_ARCH} CACHE INTERNAL "FOOBAR WORKAROUND")
endif()
endif()
# In conjunction with the variables below this makes all find_* commands
# only look in the DK
set(CMAKE_FIND_ROOT_PATH "${DEVELOPMENT_KIT_ROOT}")
# Make the find_file command only look in the DK
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY)
# Make the find_package command only look in the DK
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Make the find_library command only look in the DK
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# Make the find_package command only look in the DK
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
I've done my best to sanitize my companies work and still providing a meaningful toolchain to the CMake team.
This toolchain is currently tested against Vs2019/Ninja. However it's only the Ninja path that is having problems.
So currently all 5 of these variables are necessary to compile our project with an MSVC toolchain.
set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "14" CACHE INTERNAL "")
set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "90" CACHE INTERNAL "")
# These are necessary for RC files
set(CMAKE_CL_SHOWINCLUDES_PREFIX "Note: including file: " CACHE INTERNAL "FOOBAR WORKAROUND")
set(CMAKE_CXX_ARCHITECTURE_ID "x64" CACHE INTERNAL "FOOBAR WORKAROUND")
set(CMAKE_C_ARCHITECTURE_ID "x64" CACHE INTERNAL "FOOBAR WORKAROUND")
Should CMake toolchain authors be aware of these variables?
Edited by Brad King