Repository: bazelbuild/bazel
Issue: Windows, C++: support selecting the Windows SDK
Labels: type: feature request
P2
area-Windows
team-OSS

### Feature requests: what underlying problem are you trying to solve with this feature?

If multiple Windows SDK versions are installed, the user may want to select which one to use. Currently there's no way to do so.

### What operating system are you running Bazel on?

Windows 10

### What's the output of `bazel info release`?

release 0.27.0

###  Have you found anything relevant by searching the web?

Related: https://github.com/bazelbuild/bazel/issues/8353

Issue comments:
cc @meteorcloudy @hlopko 
I don't think I'll have the capacity to design/implement this, but I'll gladly review the proposal.
This won't be hard to implement it, I will do it!

Pull request review comments:
We have to explicitly use `bin\amd64\VCVARS64.BAT` because the `VCVARSALL.BAT` in VC 2015 falls back to `vcbuildtools.bat` when VC 2017 or VC 2019 is installed. But `vcbuildtools.bat` doesn't support specifying Windows SDK. I also double checked, if no Windows SDK version is passed, `vcbuildtools.bat` sets the exactly same environment variables as `VCVARSALL.BAT`.
Thanks for checking and for the comment!
I'd start with describing the default case, i.e. when `BAZEL_WINSDK_FULL_VERSION` is not specified.

Then you can elaborate what if there are multiple SDK versions installed, i.e. when the user may need to set `BAZEL_WINSDK_FULL_VERSION`.
> or specify 8.1 to use the Windows 8.1 SDK

This suggests 8.1 is the only acceptable value for the Windows 8.1 SDK. What I guess you meant is:

> or specify a Windows OS version to use the latest SDK installed for that (e.g. 8.1 to use the latest installed Windows 8.1 SDK)
Is there a vcvars64.bat in this case, and if so, shall we use it here too? We only support 64 bit anyway.
That's actually what I meant, there's no subversion for Windows 8.1 SDK, only one version is available. I copied this from the official documentation. ;)
https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=vs-2019
That's a good idea! 
Fun fact: in VC 2015, `VCVARSALL.BAT` calls platform specific scripts (`VCVARS64.bat`, ect), in VC 2017 & 2019, platform specific scripts call `VCVARSALL.BAT`...
Thanks, done!
Nice.
I see, thanks! Could you maybe call this out in the doc explicitly?
Done.
Replace "Note that:" with "Requirement:" to make it stand out more.
Could you add a brief reasoning? (Enough to say that VS 2015 build tools doesn't support selecting the SDK.)
`||` syntax is not supported in Cmd, you must do this instead (yes, with a single `&`):

    bla bla & if "%errorlevel%" neq "0" (exit /b 1)
Can you replace this whole function with just this? :

    return repository_ctx.os.environ.get("BAZEL_WINSDK_FULL_VERSION", default = "")
Sure!
Just realized the `exit /b` part is not needed. But thanks for the suggestion!
Done, thanks!

Patch that resolved the issue:
diff --git a/site/docs/windows.md b/site/docs/windows.md
index 262cc569ead958..3e128ace0db331 100644
--- a/site/docs/windows.md
+++ b/site/docs/windows.md
@@ -184,7 +184,19 @@ To build C++ targets, you need:
     SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk).
 
     The Windows SDK contains header files and libraries you need when building
-    Windows applications, including Bazel itself.
+    Windows applications, including Bazel itself. By default, the latest Windows SDK installed will
+    be used. You also can specify Windows SDK version by setting `BAZEL_WINSDK_FULL_VERSION`. You
+    can use a full Windows 10 SDK number such as 10.0.10240.0, or specify 8.1 to use the Windows 8.1
+    SDK (only one version of Windows 8.1 SDK is available). Please make sure you have the specified
+    Windows SDK installed.
+
+    *Requirement*: This is supported with VC 2017 and 2019. The standalone VC 2015 Build Tools doesn't
+    support selecting Windows SDK, you'll need the full Visual Studio 2015 installation, otherwise
+    `BAZEL_WINSDK_FULL_VERSION` will be ignored.
+
+    ```
+    set BAZEL_WINSDK_FULL_VERSION=10.0.10240.0
+    ```
 
 If everything is set up, you can build a C++ target now!
 
diff --git a/tools/cpp/cc_configure.bzl b/tools/cpp/cc_configure.bzl
index 66dce02f991fde..b2e2080c196dfa 100644
--- a/tools/cpp/cc_configure.bzl
+++ b/tools/cpp/cc_configure.bzl
@@ -139,6 +139,7 @@ cc_autoconf = repository_rule(
         "BAZEL_VC",
         "BAZEL_VC_FULL_VERSION",
         "BAZEL_VS",
+        "BAZEL_WINSDK_FULL_VERSION",
         "BAZEL_LLVM",
         "USE_CLANG_CL",
         "CC",
diff --git a/tools/cpp/windows_cc_configure.bzl b/tools/cpp/windows_cc_configure.bzl
index dcdf3d6f843ee8..c5528ac219de3a 100644
--- a/tools/cpp/windows_cc_configure.bzl
+++ b/tools/cpp/windows_cc_configure.bzl
@@ -229,32 +229,57 @@ def _is_vs_2017_or_2019(vc_path):
     # C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\
     return vc_path.find("2017") != -1 or vc_path.find("2019") != -1
 
-def _find_vcvarsall_bat_script(repository_ctx, vc_path):
-    """Find vcvarsall.bat script. Doesn't %-escape the result."""
+def _find_vcvars_bat_script(repository_ctx, vc_path):
+    """Find batch script to set up environment variables for VC. Doesn't %-escape the result."""
     if _is_vs_2017_or_2019(vc_path):
-        vcvarsall = vc_path + "\\Auxiliary\\Build\\VCVARSALL.BAT"
+        vcvars_script = vc_path + "\\Auxiliary\\Build\\VCVARSALL.BAT"
     else:
-        vcvarsall = vc_path + "\\VCVARSALL.BAT"
+        vcvars_script = vc_path + "\\VCVARSALL.BAT"
 
-    if not repository_ctx.path(vcvarsall).exists:
+    if not repository_ctx.path(vcvars_script).exists:
         return None
 
-    return vcvarsall
+    return vcvars_script
+
+def _is_support_winsdk_selection(repository_ctx, vc_path):
+    """Windows SDK selection is supported with VC 2017 / 2019 or with full VS 2015 installation."""
+    if _is_vs_2017_or_2019(vc_path):
+        return True
+    # By checking the source code of VCVARSALL.BAT in VC 2015, we know that
+    # when devenv.exe or wdexpress.exe exists, VCVARSALL.BAT supports Windows SDK selection.
+    vc_common_ide = repository_ctx.path(vc_path).dirname.get_child("Common7").get_child("IDE")
+    for tool in ["devenv.exe", "wdexpress.exe"]:
+        if vc_common_ide.get_child(tool).exists:
+            return True
+    return False
 
 def setup_vc_env_vars(repository_ctx, vc_path):
-    """Get environment variables set by VCVARSALL.BAT. Doesn't %-escape the result!"""
-    vcvarsall = _find_vcvarsall_bat_script(repository_ctx, vc_path)
-    if not vcvarsall:
-        return None
+    """Get environment variables set by VCVARSALL.BAT script. Doesn't %-escape the result!"""
+    vcvars_script = _find_vcvars_bat_script(repository_ctx, vc_path)
+    if not vcvars_script:
+        auto_configure_fail("Cannot find VCVARSALL.BAT script under %s" % vc_path)
+
+    # Getting Windows SDK version set by user.
+    # Only supports VC 2017 & 2019 and VC 2015 with full VS installation.
+    winsdk_version = _get_winsdk_full_version(repository_ctx)
+    if winsdk_version and not _is_support_winsdk_selection(repository_ctx, vc_path):
+        auto_configure_warning(("BAZEL_WINSDK_FULL_VERSION=%s is ignored, " +
+            "because standalone Visual C++ Build Tools 2015 doesn't support specifying Windows " +
+            "SDK version, please install the full VS 2015 or use VC 2017/2019.") % winsdk_version)
+        winsdk_version = ""
+
+    # Get VC version set by user. Only supports VC 2017 & 2019.
     vcvars_ver = ""
     if _is_vs_2017_or_2019(vc_path):
         full_version = _get_vc_full_version(repository_ctx, vc_path)
         if full_version:
             vcvars_ver = "-vcvars_ver=" + full_version
+
+    cmd = "\"%s\" amd64 %s %s" % (vcvars_script, winsdk_version, vcvars_ver)
     repository_ctx.file(
         "get_env.bat",
         "@echo off\n" +
-        ("call \"%s\" amd64 %s > NUL \n" % (vcvarsall, vcvars_ver)) +
+        ("call %s > NUL \n" % cmd) +
         "echo PATH=%PATH%,INCLUDE=%INCLUDE%,LIB=%LIB%,WINDOWSSDKDIR=%WINDOWSSDKDIR% \n",
         True,
     )
@@ -267,8 +292,16 @@ def setup_vc_env_vars(repository_ctx, vc_path):
     for env in envs:
         key, value = env.split("=", 1)
         env_map[key] = escape_string(value.replace("\\", "\\\\"))
+    _check_env_vars(env_map, cmd)
     return env_map
 
+def _check_env_vars(env_map, cmd):
+    envs = ["PATH", "INCLUDE", "LIB", "WINDOWSSDKDIR"]
+    for env in envs:
+        if not env_map.get(env):
+            auto_configure_fail(
+                "Setting up VC environment variables failed, %s is not set by the following command:\n    %s" % (env, cmd))
+
 def _get_latest_subversion(repository_ctx, vc_path):
     """Get the latest subversion of a VS 2017/2019 installation.
 
@@ -300,6 +333,10 @@ def _get_vc_full_version(repository_ctx, vc_path):
         return repository_ctx.os.environ["BAZEL_VC_FULL_VERSION"]
     return _get_latest_subversion(repository_ctx, vc_path)
 
+def _get_winsdk_full_version(repository_ctx):
+    """Return the value of BAZEL_WINSDK_FULL_VERSION if defined, otherwise an empty string."""
+    return repository_ctx.os.environ.get("BAZEL_WINSDK_FULL_VERSION", default = "")
+
 def find_msvc_tool(repository_ctx, vc_path, tool):
     """Find the exact path of a specific build tool in MSVC. Doesn't %-escape the result."""
     tool_path = None
@@ -320,7 +357,7 @@ def find_msvc_tool(repository_ctx, vc_path, tool):
 def _find_missing_vc_tools(repository_ctx, vc_path):
     """Check if any required tool is missing under given VC path."""
     missing_tools = []
-    if not _find_vcvarsall_bat_script(repository_ctx, vc_path):
+    if not _find_vcvars_bat_script(repository_ctx, vc_path):
         missing_tools.append("VCVARSALL.BAT")
 
     for tool in ["cl.exe", "link.exe", "lib.exe", "ml64.exe"]:


Test patch: