Repository: conan-io/conan
Pull request: adding tools.microsoft:winsdk_version conf
Changelog: Feature: Adding ``tools.microsoft:winsdk_version`` conf to make ``VCVars`` generator to use the given ``winsdk_version``.
Docs: https://github.com/conan-io/docs/pull/3487

Close https://github.com/conan-io/conan/issues/14383


Problem statement:
[bug] It's impossible to set winsdk_version argument for vcvars script
### Environment details

* Windows 11
* MSVC 2022
* Conan version: 2.0.9
* Python version: 3.10



### Steps to reproduce

The generated `conanvcvars.bat` contains the following line

```
set "VSCMD_START_DIR=%CD%" && call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC/Auxiliary/Build/vcvarsall.bat"  amd64 -vcvars_ver=14.3
```

`winsdk_version` is not set at all.
For some legacy code I need to set older SDK version for MSVC among several installed.
Please, correct me if I wrong, but I looked through the documentation and python scripts and it's looks like it's impossible to set it.

Thank you.

### Logs

_No response_

Hints from issue discussion:
Hi @apyvovar 

Yes, you are right, at the moment there is no way to set the ``winsdk_version``.

It shouldn't be difficult to define a way, but I am not sure what would be the right mechanism to input it:
- Is it a ``conf`` like ``tools.microsoft:winsdk_version`` that the ``VCVars`` generator reads it? Or is it an optional ``settings.yml`` subsetting for the ``os=Windows``?
- What other generators need to received it? Is it ``CMAKE_SYSTEM_VERSION`` for cmake, or better ``-T v143,version=14.31.31103``?
- How MSBuild, Meson or Autotools could define this?

He, @memsharded 

IMO, since we already have `tools.microsoft.msbuild:vs_version` section in `conf`, it's seems natural to add `winsdk_version` there as well.

Unfortunately, I didn't have change to check it on other generators, so I can't say the way to implement it them.

Hope, the issue will be resolved soon.

Thank you.
> IMO, since we already have tools.microsoft.msbuild:vs_version

This is the discussion. We know that the ``vs_version`` given you use the same toolset won't produce different binaries. So it is important to know if the ``winsdk_version`` could affect somehow and produce different binaries. Apparently that this is not the case (can @jcar87 confirm?) and the ``winsdk_version`` could be a ``conf``.

``Meson``, ``Autotools`` and ``MSBuild`` Toolchains will be ok, because they use ``VCVars`` under the hood too. But ``CMakeToolchain`` we really need to know how to do it before proceeding, this is a must if we want to support this new conf.
The Windows SDK version can produce different binaries. I'm not sure if on its own, but there are libraries that check preprocessor macros from the Windows SDK and include different code depending on their existence/value. Therefore, we currently have that in `options` (`settings` would make more sense though, as already done for the Apple SDKs).

We currently (with Conan v1.60.2) use it for the CMake and MSBuild/VisualStudio build helpers. For CMake versions up to 3.26, the setting is `CMAKE_SYSTEM_VERSION`, but that changed with https://gitlab.kitware.com/cmake/cmake/-/merge_requests/8389 (I haven't updated to 3.27 yet, so I'm not familiar with the new way).
> The Windows SDK version can produce different binaries. I'm not sure if on its own, but there are libraries that check preprocessor macros from the Windows SDK and include different code depending on their existence/value. Therefore, we currently have that in `options` (`settings` would make more sense though, as already done for the Apple SDKs).

This appears to be unclear from the microsoft documentation however, it appear that as you say, the keyword is "on its own" - the current windows 10 SDKs are compatible with Windows 7 SP1 onwards, as per https://learn.microsoft.com/en-us/cpp/windows/how-to-use-the-windows-10-sdk-in-a-windows-desktop-application?view=msvc-170


According to this [stackoverflow](https://stackoverflow.com/questions/39621646/what-does-the-target-platform-version-mean-for-a-vs-c-project/56084339#56084339), in Visual Studio 2019, the "Target Platform Version" field in the C++ projects was renamed to "Windows SDK Version". I would guess this is a good thing since otherwise it seems confusing, and may imply that binaries will only work with that version of Windows.



When features from specific versions are required, one can 'target" newer versions of Windows:
> For more information about targeting specific versions of Windows, see [Using the Windows Headers](https://learn.microsoft.com/en-us/windows/win32/WinProg/using-the-windows-headers) and [Update WINVER and _WIN32_WINNT](https://learn.microsoft.com/en-us/cpp/porting/modifying-winver-and-win32-winnt?view=msvc-170).

This is indeed done with macros.  

There's some additional information here: https://walbourn.github.io/a-brief-history-of-windows-sdks/  

What I take from all this:
* Using a recent version of the SDK doesn't necessarily make binaries incompatible with older versions of Windows, this depends on the actual API calls made by the code. Visual Studio, now CMake (as per the recent 3.27 update) and vcvars all choose the most recent version of the SDK available, so I think that's a strong indicator that this is not a big issue and using a recent version is recommended. 
* The SDKs support a range of versions, with the lower bound being Windows 7 SP1 and the upper bound being some arbitrary Windows 10/Windows 11 version (presumably the one matching the version of the SDK itself).

Lets move this forward:
- New conf to define it
- Users that want to make it part of the ``package_id`` can do it via other ``conf``
- Vcvars itnegration is easy
- @jcar87 will help with CMake integration
- @memsharded will check MSBuildToolchain integration
Hi, I'm looking for this support to. I can give some inputs, since I already spend some time on debugging:

- Conan settings : apple platforms in settings.yml already support `os.sdk_version`, what about to add same for Windows
- CMake: 
    - sdk is selected based on this conditions: [CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION](https://cmake.org/cmake/help/latest/variable/CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION.html) implemented here [cmGlobalVisualStudio14Generator.cxx](https://github.com/Kitware/CMake/blob/master/Source/cmGlobalVisualStudio14Generator.cxx#L142)
    - [CMAKE_SYSTEM_VERSION](https://cmake.org/cmake/help/latest/variable/CMAKE_SYSTEM_VERSION.html#variable:CMAKE_SYSTEM_VERSION) is important
        - for x86/x86_64 CMAKE_SYSTEM_VERSION defaults to CMAKE_HOST_SYSTEM_VERSION aka conan build context
        - for arm/arm64 (Cross Compiling) has no default, in this case, a lot of CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION conditions are not valid

Update: @memsharded 
- Looks like for cmaketoolchain this is already supported in Conan 1.X with [tools.cmake.cmaketoolchain:system_version](https://github.com/conan-io/conan/blob/release/2.0/conan/tools/cmake/toolchain/blocks.py#L744) when cross building
- And for WindowsStore/WindowsCE, settings.yml already support `os.version: ["8.1", "10.0"]`/`os.version: ["5.0", "6.0", "7.0", "8.0"]
` which will be used for [CMAKE_SYSTEM_VERSION](https://github.com/conan-io/conan/blob/release/2.0/conan/tools/cmake/toolchain/blocks.py#L764) when cross building. So just updating settings.yml for Windows can achieve same
- If this would not be only for cross building, it can be used for x86/x86_64 on windows
- also documentation not mention that `tools.cmake.cmaketoolchain:system_...` are only for cross build

Note: In my case, I try to build windows arm64 and since I have both winsdk 8.1 and 10.0 installed and CMAKE_SYSTEM_VERSION is not set, CMake choose 8.1 :-(

Can this be ported also into Conan 1.X?
Proposing initial draft PR in https://github.com/conan-io/conan/pull/15272

Commit message: fix

Patch to review:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index ad5e5ee7484..c587534edf6 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -577,7 +577,7 @@ def context(self):
                                                             check_type=bool)
         if android_ndk_path and (cxxflags or cflags) and android_legacy_toolchain is not False:
             self._conanfile.output.warning("tools.build:cxxflags or cflags are defined, but Android NDK toolchain may be overriding "
-                                            "the values. Consider setting tools.android:cmake_legacy_toolchain to False.")
+                                           "the values. Consider setting tools.android:cmake_legacy_toolchain to False.")
 
         return {
             "cxxflags": cxxflags,
@@ -641,6 +641,9 @@ def context(self):
 
 class GenericSystemBlock(Block):
     template = textwrap.dedent("""
+        ########## generic_system block #############
+        # Definition of system, platform and toolset
+        #############################################
         {% if cmake_sysroot %}
         set(CMAKE_SYSROOT {{ cmake_sysroot }})
         {% endif %}
@@ -656,10 +659,24 @@ class GenericSystemBlock(Block):
         set(CMAKE_SYSTEM_PROCESSOR {{ cmake_system_processor }})
         {% endif %}
 
-        {% if generator_platform %}
+        {% if generator_platform and not winsdk_version %}
         set(CMAKE_GENERATOR_PLATFORM "{{ generator_platform }}" CACHE STRING "" FORCE)
+        {% elif winsdk_version %}
+        if(POLICY CMP0149)
+            cmake_policy(GET CMP0149 _POLICY_WINSDK_VERSION)
+        endif()
+        if(_POLICY_WINSDK_VERSION STREQUAL "NEW")
+            message(STATUS "Conan toolchain: CMAKE_GENERATOR_PLATFORM={{gen_platform_sdk_version}}")
+            set(CMAKE_GENERATOR_PLATFORM "{{ gen_platform_sdk_version }}" CACHE STRING "" FORCE)
+        else()
+            # winsdk_version will be taken from above CMAKE_SYSTEM_VERSION
+            message(STATUS "Conan toolchain: CMAKE_GENERATOR_PLATFORM={{generator_platform}}")
+            set(CMAKE_GENERATOR_PLATFORM "{{ generator_platform }}" CACHE STRING "" FORCE)
+        endif()
         {% endif %}
+
         {% if toolset %}
+        message(STATUS "Conan toolchain: CMAKE_GENERATOR_TOOLSET={{ toolset }}")
         set(CMAKE_GENERATOR_TOOLSET "{{ toolset }}" CACHE STRING "" FORCE)
         {% endif %}
         """)
@@ -781,12 +798,32 @@ def context(self):
         cmake_sysroot = self._conanfile.conf.get("tools.build:sysroot")
         cmake_sysroot = cmake_sysroot.replace("\\", "/") if cmake_sysroot is not None else None
 
+        winsdk_version = self._conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str)
+        if winsdk_version:
+            if system_version:
+                self._conanfile.output.warning("Both cmake_system_version and winsdk_version confs"
+                                               " defined, prioritizing winsdk_version")
+            system_version = winsdk_version
+        elif "Windows" in self._conanfile.settings.get_safe("os", ""):
+            winsdk_version = self._conanfile.settings.get_safe("os.version")
+            if system_version:
+                if winsdk_version:
+                    self._conanfile.output.warning("Both cmake_system_version conf and os.version"
+                                                   " defined, prioritizing cmake_system_version")
+                winsdk_version = system_version
+
+        gen_platform_sdk_version = [generator_platform,
+                                    f"version={winsdk_version}" if winsdk_version else None]
+        gen_platform_sdk_version = ",".join(d for d in gen_platform_sdk_version if d)
+
         return {"toolset": toolset,
                 "generator_platform": generator_platform,
                 "cmake_system_name": system_name,
                 "cmake_system_version": system_version,
                 "cmake_system_processor": system_processor,
-                "cmake_sysroot": cmake_sysroot}
+                "cmake_sysroot": cmake_sysroot,
+                "winsdk_version": winsdk_version,
+                "gen_platform_sdk_version": gen_platform_sdk_version}
 
 
 class OutputDirsBlock(Block):
diff --git a/conan/tools/microsoft/toolchain.py b/conan/tools/microsoft/toolchain.py
index 395dcd1ba91..e5427001cae 100644
--- a/conan/tools/microsoft/toolchain.py
+++ b/conan/tools/microsoft/toolchain.py
@@ -37,6 +37,9 @@ class MSBuildToolchain(object):
             </ResourceCompile>
           </ItemDefinitionGroup>
           <PropertyGroup Label="Configuration">
+            {% if winsdk_version %}
+            <WindowsTargetPlatformVersion>{{ winsdk_version}}</WindowsTargetPlatformVersion>
+            {% endif %}
             <PlatformToolset>{{ toolset }}</PlatformToolset>
             {% for k, v in properties.items() %}
             <{{k}}>{{ v }}</{{k}}>
@@ -149,6 +152,10 @@ def format_macro(key, value):
                  "\n      <ProcessorNumber>{}</ProcessorNumber>".format(njobs)])
         compile_options = "".join("\n      <{k}>{v}</{k}>".format(k=k, v=v)
                                   for k, v in self.compile_options.items())
+
+        winsdk_version = self._conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str)
+        winsdk_version = winsdk_version or self._conanfile.settings.get_safe("os.version")
+
         return {
             'defines': defines,
             'compiler_flags': " ".join(self.cxxflags + self.cflags),
@@ -159,6 +166,7 @@ def format_macro(key, value):
             "compile_options": compile_options,
             "parallel": parallel,
             "properties": self.properties,
+            "winsdk_version": winsdk_version
         }
 
     def _write_config_toolchain(self, config_filename):
diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py
index e485376ca4d..157ca4833da 100644
--- a/conan/tools/microsoft/visual.py
+++ b/conan/tools/microsoft/visual.py
@@ -132,21 +132,23 @@ def generate(self, scope="build"):
             vcvars_ver = _vcvars_vers(conanfile, compiler, vs_version)
         vcvarsarch = _vcvars_arch(conanfile)
 
+        winsdk_version = conanfile.conf.get("tools.microsoft:winsdk_version", check_type=str)
+        winsdk_version = winsdk_version or conanfile.settings.get_safe("os.version")
         # The vs_install_path is like
         # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
         # C:\Program Files (x86)\Microsoft Visual Studio\2017\Community
         # C:\Program Files (x86)\Microsoft Visual Studio 14.0
         vcvars = vcvars_command(vs_version, architecture=vcvarsarch, platform_type=None,
-                                winsdk_version=None, vcvars_ver=vcvars_ver,
+                                winsdk_version=winsdk_version, vcvars_ver=vcvars_ver,
                                 vs_install_path=vs_install_path)
 
         content = textwrap.dedent("""\
             @echo off
             set __VSCMD_ARG_NO_LOGO=1
             set VSCMD_SKIP_SENDTELEMETRY=1
-            echo conanvcvars.bat: Activating environment Visual Studio {} - {} - vcvars_ver={}
+            echo conanvcvars.bat: Activating environment Visual Studio {} - {} - winsdk_version={} - vcvars_ver={}
             {}
-            """.format(vs_version, vcvarsarch, vcvars_ver, vcvars))
+            """.format(vs_version, vcvarsarch, winsdk_version, vcvars_ver, vcvars))
         from conan.tools.env.environment import create_env_script
         create_env_script(conanfile, content, CONAN_VCVARS_FILE, scope)
 
diff --git a/conans/model/conf.py b/conans/model/conf.py
index ec8711e9e5d..a21f810f669 100644
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -80,6 +80,7 @@
     "tools.google.bazel:bazelrc_path": "List of paths to bazelrc files to be used as 'bazel --bazelrc=rcpath1 ... build'",
     "tools.meson.mesontoolchain:backend": "Any Meson backend: ninja, vs, vs2010, vs2012, vs2013, vs2015, vs2017, vs2019, xcode",
     "tools.meson.mesontoolchain:extra_machine_files": "List of paths for any additional native/cross file references to be appended to the existing Conan ones",
+    "tools.microsoft:winsdk_version": "Use this winsdk_version in vcvars",
     "tools.microsoft.msbuild:vs_version": "Defines the IDE version (15, 16, 17) when using the msvc compiler. Necessary if compiler.version specifies a toolset that is not the IDE default",
     "tools.microsoft.msbuild:max_cpu_count": "Argument for the /m when running msvc to build parallel projects",
     "tools.microsoft.msbuild:installation_path": "VS install path, to avoid auto-detect via vswhere, like C:/Program Files (x86)/Microsoft Visual Studio/2019/Community. Use empty string to disable",
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index be6dfc837cc..68f2908ff0d 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -555,6 +555,20 @@ def test_cmake_toolchain_runtime_types_cmake_older_than_3_15():
     assert "LIBCMTD" in client.out
 
 
+@pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows")
+# @pytest.mark.tool("cmake", "3.28")
+def test_cmake_toolchain_winsdk_version():
+    client = TestClient(path_with_spaces=False)
+    client.run("new cmake_lib -d name=hello -d version=0.1")
+    # cmake = client.load("CMakeLists.txt")
+    # cmake = cmake.replace("cmake_minimum_required(VERSION 3.15)",
+    #                       "cmake_minimum_required(VERSION 3.27)")
+    # client.save({"CMakeLists.txt": cmake})
+    client.run("create . -s arch=x86_64 -s compiler.version=193 "
+               "-c tools.microsoft:winsdk_version=8.1")
+    assert "Conan toolchain: CMAKE_GENERATOR_PLATFORM=x64" in client.out
+
+
 @pytest.mark.tool("cmake", "3.23")
 def test_cmake_presets_missing_option():
     client = TestClient(path_with_spaces=False)
diff --git a/conans/test/functional/toolchains/microsoft/test_msbuildtoolchain.py b/conans/test/functional/toolchains/microsoft/test_msbuildtoolchain.py
index b11d4fa4ae4..2d454d50099 100644
--- a/conans/test/functional/toolchains/microsoft/test_msbuildtoolchain.py
+++ b/conans/test/functional/toolchains/microsoft/test_msbuildtoolchain.py
@@ -1,25 +1,12 @@
 import platform
-import sys
 import textwrap
-import os
 
 import pytest
-try:
-    from unittest.mock import MagicMock
-except:
-    from mock import MagicMock
 
-from conan.tools.files import replace_in_file
 from conans.test.utils.tools import TestClient
 
-toolchain_props = """
-  <ImportGroup Label="PropertySheets">
-      <Import Project="conan\\conantoolchain_release_x64.props" />
-"""
 
-
-@pytest.mark.skipif(sys.version_info.major == 2, reason="Meson not supported in Py2")
-@pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows")
+@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
 def test_msbuildtoolchain_props_with_extra_flags():
     """
     Real test which is injecting some compiler/linker options and other dummy defines and
@@ -39,14 +26,25 @@ def test_msbuildtoolchain_props_with_extra_flags():
     """)
     client = TestClient(path_with_spaces=False)
     client.run("new msbuild_exe -d name=hello -d version=0.1")
-    client.save({
-        "myprofile": profile
-    })
-    # Let's import manually the created conantoolchain_release_x64.props
-    replace_in_file(MagicMock(), os.path.join(client.current_folder, "hello.vcxproj"),
-                    r'  <ImportGroup Label="PropertySheets">', toolchain_props)
-    client.run("create . -pr myprofile -tf=\"\"")
+    client.save({"myprofile": profile})
+    #  conantoolchain.props is already imported in the msbuild_exe tempalte
+    client.run("create . -pr myprofile -tf=")
     assert "/analyze:quiet /doc src/hello.cpp" in client.out
     assert r"/VERBOSE:UNUSEDLIBS /PDB:mypdbfile x64\Release\hello.obj" in client.out
     assert "/D DEF1 /D DEF2" in client.out
     assert "Build succeeded." in client.out
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
+def test_msbuildtoolchain_winsdk_version():
+    """
+    Configure sdk_version
+    """
+    client = TestClient(path_with_spaces=False)
+    client.run("new msbuild_lib -d name=hello -d version=0.1")
+    #  conantoolchain.props is already imported in the msbuild_exe tempalte
+    client.run("create . -s arch=x86_64 -s compiler.version=193 "
+               "-c tools.microsoft:winsdk_version=8.1")
+    # I have verified also opening VS IDE that the setting is correctly configured
+    # because the test always run over vcvars that already activates it
+    assert "amd64 - winsdk_version=8.1 - vcvars_ver=14.3" in client.out
diff --git a/conans/test/integration/toolchains/microsoft/vcvars_test.py b/conans/test/integration/toolchains/microsoft/vcvars_test.py
index eb43b63e9ee..d17f42cee1a 100644
--- a/conans/test/integration/toolchains/microsoft/vcvars_test.py
+++ b/conans/test/integration/toolchains/microsoft/vcvars_test.py
@@ -107,3 +107,22 @@ class TestConan(ConanFile):
     vcvars = client.load("conanvcvars.bat")
     assert 'vcvarsall.bat"  x86_amd64' in vcvars
     assert "-vcvars_ver" not in vcvars
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
+def test_vcvars_winsdk_version():
+    client = TestClient(path_with_spaces=False)
+
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class TestConan(ConanFile):
+            generators = "VCVars"
+            settings = "os", "compiler", "arch", "build_type"
+    """)
+    client.save({"conanfile.py": conanfile})
+    client.run('install . -s os=Windows -s compiler=msvc -s compiler.version=193 '
+               '-s compiler.cppstd=14 -s compiler.runtime=static '
+               '-c tools.microsoft:winsdk_version=8.1')
+
+    vcvars = client.load("conanvcvars.bat")
+    assert 'vcvarsall.bat"  amd64 8.1 -vcvars_ver=14.3' in vcvars


Code review comments:
diff_hunk: @@ -555,6 +555,20 @@ def test_cmake_toolchain_runtime_types_cmake_older_than_3_15():
     assert "LIBCMTD" in client.out
 
 
+@pytest.mark.skipif(platform.system() != "Windows", reason="Only for windows")
+# @pytest.mark.tool("cmake", "3.28")
+def test_cmake_toolchain_winsdk_version():
+    client = TestClient(path_with_spaces=False)
+    client.run("new cmake_lib -d name=hello -d version=0.1")
+    # cmake = client.load("CMakeLists.txt")
+    # cmake = cmake.replace("cmake_minimum_required(VERSION 3.15)",
+    #                       "cmake_minimum_required(VERSION 3.27)")
+    # client.save({"CMakeLists.txt": cmake})
+    client.run("create . -s arch=x86_64 -s compiler.version=193 "
+               "-c tools.microsoft:winsdk_version=8.1")
+    assert "Conan toolchain: CMAKE_GENERATOR_PLATFORM=x64" in client.out
path: conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
text: @user1:
In the CMake lists we can print the value of `CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION` - which is a read-only variable of the version of the SDK that CMake selected - although I'm not 100% what happens if you choose a version that you don't have installed

@author:
If you pick a version that you don't have installed, with the OLD policy, it will fallback to the latest SDK

@author:
I'll add that check, good idea.