SDL: Setup msvc environment on Windows

From 28e01c27557244d39cf84477e5d50dc2894ad636 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Sat, 17 Jun 2023 22:26:45 +0200
Subject: [PATCH] Setup msvc environment on Windows

---
 packed/index.js | 297 +++++++++++++++++++++++++++++++++++++++++++++---
 src/main.ts     |   8 ++
 src/msvc.ts     | 294 +++++++++++++++++++++++++++++++++++++++++++++++
 src/ninja.ts    |  11 +-
 src/platform.ts |  13 ---
 5 files changed, 588 insertions(+), 35 deletions(-)
 create mode 100644 src/msvc.ts

diff --git a/packed/index.js b/packed/index.js
index c6ffc3f91af6..9e6b4964858f 100644
--- a/packed/index.js
+++ b/packed/index.js
@@ -85,6 +85,7 @@ const child_process = __importStar(__nccwpck_require__(2081));
 const fs = __importStar(__nccwpck_require__(7147));
 const core = __importStar(__nccwpck_require__(2186));
 const constants_1 = __nccwpck_require__(7077);
+const msvc_1 = __nccwpck_require__(1983);
 const ninja_1 = __nccwpck_require__(6526);
 const util_1 = __nccwpck_require__(9731);
 const version_1 = __nccwpck_require__(6970);
@@ -184,6 +185,11 @@ async function run() {
             await (0, ninja_1.configure_ninja_build_tool)(SDL_BUILD_PLATFORM);
         });
     }
+    if (SDL_BUILD_PLATFORM == platform_1.SdlBuildPlatform.Windows) {
+        await core.group(`Configuring VS environment`, async () => {
+            (0, msvc_1.setup_vc_environment)();
+        });
+    }
     const source_dir = `${SETUP_SDL_ROOT}/src`;
     const build_dir = `${SETUP_SDL_ROOT}/build`;
     const install_dir = `${SETUP_SDL_ROOT}`;
@@ -198,6 +204,280 @@ async function run() {
 run();
 
 
+/***/ }),
+
+/***/ 1983:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+// Copyright 2019 ilammy
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+    Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+    o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+    __setModuleDefault(result, mod);
+    return result;
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.setup_vc_environment = void 0;
+const child_process = __importStar(__nccwpck_require__(2081));
+const fs = __importStar(__nccwpck_require__(7147));
+const path = __importStar(__nccwpck_require__(1017));
+const process = __importStar(__nccwpck_require__(7282));
+const core = __importStar(__nccwpck_require__(2186));
+const util_1 = __nccwpck_require__(9731);
+const PROGRAM_FILES_X86 = process.env["ProgramFiles(x86)"];
+const PROGRAM_FILES = [
+    process.env["ProgramFiles(x86)"],
+    process.env["ProgramFiles"],
+];
+const EDITIONS = ["Enterprise", "Professional", "Community"];
+const YEARS = ["2022", "2019", "2017"];
+const VsYearVersion = {
+    "2022": "17.0",
+    "2019": "16.0",
+    "2017": "15.0",
+    "2015": "14.0",
+    "2013": "12.0",
+};
+const VSWHERE_PATH = `${PROGRAM_FILES_X86}\\Microsoft Visual Studio\\Installer`;
+function vsversion_to_versionnumber(vsversion) {
+    if (Object.values(VsYearVersion).includes(vsversion)) {
+        return vsversion;
+    }
+    else {
+        if (vsversion in VsYearVersion) {
+            return VsYearVersion[vsversion];
+        }
+    }
+    return vsversion;
+}
+function vsversion_to_year(vsversion) {
+    if (Object.keys(VsYearVersion).includes(vsversion)) {
+        return vsversion;
+    }
+    else {
+        for (const [year, ver] of Object.entries(VsYearVersion)) {
+            if (ver === vsversion) {
+                return year;
+            }
+        }
+    }
+    return vsversion;
+}
+function findWithVswhere(pattern, version_pattern) {
+    try {
+        const installationPath = child_process
+            .execSync(`vswhere -products * ${version_pattern} -prerelease -property installationPath`)
+            .toString()
+            .trim();
+        return `${installationPath}\\\\${pattern}`;
+    }
+    catch (e) {
+        core.warning(`vswhere failed: ${e}`);
+    }
+    return null;
+}
+function findVcvarsall(vsversion) {
+    const vsversion_number = vsversion_to_versionnumber(vsversion);
+    let version_pattern;
+    if (vsversion_number) {
+        const upper_bound = vsversion_number.split(".")[0] + ".9";
+        version_pattern = `-version "${vsversion_number},${upper_bound}"`;
+    }
+    else {
+        version_pattern = "-latest";
+    }
+    // If vswhere is available, ask it about the location of the latest Visual Studio.
+    let path = findWithVswhere("VC\\Auxiliary\\Build\\vcvarsall.bat", version_pattern);
+    if (path && fs.existsSync(path)) {
+        core.info(`Found with vswhere: ${path}`);
+        return path;
+    }
+    core.info("Not found with vswhere");
+    // If that does not work, try the standard installation locations,
+    // starting with the latest and moving to the oldest.
+    const years = vsversion ? [vsversion_to_year(vsversion)] : YEARS;
+    for (const prog_files of PROGRAM_FILES) {
+        for (const ver of years) {
+            for (const ed of EDITIONS) {
+                path = `${prog_files}\\Microsoft Visual Studio\\${ver}\\${ed}\\VC\\Auxiliary\\Build\\vcvarsall.bat`;
+                core.info(`Trying standard location: ${path}`);
+                if (fs.existsSync(path)) {
+                    core.info(`Found standard location: ${path}`);
+                    return path;
+                }
+            }
+        }
+    }
+    core.info("Not found in standard locations");
+    // Special case for Visual Studio 2015 (and maybe earlier), try it out too.
+    path = `${PROGRAM_FILES_X86}\\Microsoft Visual C++ Build Tools\\vcbuildtools.bat`;
+    if (fs.existsSync(path)) {
+        core.info(`Found VS 2015: ${path}`);
+        return path;
+    }
+    core.info(`Not found in VS 2015 location: ${path}`);
+    throw new util_1.SetupSdlError("Microsoft Visual Studio not found");
+}
+function isPathVariable(name) {
+    const pathLikeVariables = ["PATH", "INCLUDE", "LIB", "LIBPATH"];
+    return pathLikeVariables.indexOf(name.toUpperCase()) != -1;
+}
+function filterPathValue(path) {
+    const paths = path.split(";");
+    // Remove duplicates by keeping the first occurrence and preserving order.
+    // This keeps path shadowing working as intended.
+    function unique(value, index, self) {
+        return self.indexOf(value) === index;
+    }
+    return paths.filter(unique).join(";");
+}
+/** See https://github.com/ilammy/msvc-dev-cmd#inputs */
+function setupMSVCDevCmd(arch, sdk, toolset, uwp, spectre, vsversion) {
+    if (process.platform != "win32") {
+        core.info("This is not a Windows virtual environment, bye!");
+        return;
+    }
+    // Add standard location of "vswhere" to PATH, in case it"s not there.
+    process.env.PATH += path.delimiter + VSWHERE_PATH;
+    // There are all sorts of way the architectures are called. In addition to
+    // values supported by Microsoft Visual C++, recognize some common aliases.
+    const arch_aliases = {
+        win32: "x86",
+        win64: "x64",
+        x86_64: "x64",
+        "x86-64": "x64",
+    };
+    // Ignore case when matching as that"s what humans expect.
+    if (arch.toLowerCase() in arch_aliases) {
+        arch = arch_aliases[arch.toLowerCase()];
+    }
+    // Due to the way Microsoft Visual C++ is configured, we have to resort to the following hack:
+    // Call the configuration batch file and then output *all* the environment variables.
+    const args = [arch];
+    if (uwp) {
+        args.push("uwp");
+    }
+    if (sdk) {
+        args.push(sdk);
+    }
+    if (toolset) {
+        args.push(`-vcvars_ver=${toolset}`);
+    }
+    if (spectre) {
+        args.push("-vcvars_spectre_libs=spectre");
+    }
+    const vcvars = `"${findVcvarsall(vsversion)}" ${args.join(" ")}`;
+    core.debug(`vcvars command-line: ${vcvars}`);
+    const cmd_output_string = child_process
+        .execSync(`set && cls && ${vcvars} && cls && set`, { shell: "cmd" })
+        .toString();
+    const cmd_output_parts = cmd_output_string.split("\f");
+    const old_environment = cmd_output_parts[0].split("\r\n");
+    const vcvars_output = cmd_output_parts[1].split("\r\n");
+    const new_environment = cmd_output_parts[2].split("\r\n");
+    // If vsvars.bat is given an incorrect command line, it will print out
+    // an error and *still* exit successfully. Parse out errors from output
+    // which don"t look like environment variables, and fail if appropriate.
+    const error_messages = vcvars_output.filter((line) => {
+        if (line.match(/^\[ERROR.*\]/)) {
+            // Don"t print this particular line which will be confusing in output.
+            if (!line.match(/Error in script usage. The correct usage is:$/)) {
+                return true;
+            }
+        }
+        return false;
+    });
+    if (error_messages.length > 0) {
+        throw new Error("invalid parameters" + "\r\n" + error_messages.join("\r\n"));
+    }
+    const result_vcvars = {};
+    // Convert old environment lines into a dictionary for easier lookup.
+    const old_env_vars = {};
+    for (const string of old_environment) {
+        const [name, value] = string.split("=");
+        old_env_vars[name] = value;
+    }
+    // Now look at the new environment and export everything that changed.
+    // These are the variables set by vsvars.bat. Also export everything
+    // that was not there during the first sweep: those are new variables.
+    core.startGroup("Environment variables");
+    for (const string of new_environment) {
+        const [key, vcvars_value] = string.split("=");
+        // vsvars.bat likes to print some fluff at the beginning.
+        // Skip lines that don"t look like environment variables.
+        if (!vcvars_value) {
+            continue;
+        }
+        const old_value = old_env_vars[key];
+        // For new variables "old_value === undefined".
+        if (vcvars_value !== old_value) {
+            let filtered_value = vcvars_value;
+            core.info(`Setting ${key}`);
+            // Special case for a bunch of PATH-like variables: vcvarsall.bat
+            // just prepends its stuff without checking if its already there.
+            // This makes repeated invocations of this action fail after some
+            // point, when the environment variable overflows. Avoid that.
+            if (isPathVariable(key)) {
+                filtered_value = filterPathValue(vcvars_value);
+            }
+            result_vcvars[key] = filtered_value;
+        }
+    }
+    core.endGroup();
+    core.info("Configured Developer Command Prompt");
+    return result_vcvars;
+}
+function setup_vc_environment() {
+    const arch = core.getInput("vc_arch");
+    const sdk = core.getInput("vc_sdk");
+    const toolset = core.getBooleanInput("vc_toolset");
+    const uwp = core.getBooleanInput("vc_uwp");
+    const spectre = core.getBooleanInput("vc_spectre");
+    const vsversion = core.getInput("vc_vsversion");
+    const msvc_env_vars = setupMSVCDevCmd(arch, sdk, toolset, uwp, spectre, vsversion);
+    for (const key in msvc_env_vars) {
+        process.env[key] = msvc_env_vars[key];
+    }
+}
+exports.setup_vc_environment = setup_vc_environment;
+
+
 /***/ }),
 
 /***/ 6526:
@@ -231,6 +511,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
 Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.configure_ninja_build_tool = exports.get_ninja_download_url = void 0;
 const fs = __importStar(__nccwpck_require__(7147));
+const path = __importStar(__nccwpck_require__(1017));
 const process = __importStar(__nccwpck_require__(7282));
 const core = __importStar(__nccwpck_require__(2186));
 const tc = __importStar(__nccwpck_require__(7784));
@@ -266,8 +547,7 @@ async function configure_ninja_build_tool(platform) {
         const ninja_extract_folder = await tc.extractZip(ninja_zip_path, ninja_dir);
         ninja_directory = await tc.cacheDir(ninja_extract_folder, cache_name, constants_1.NINJA_VERSION);
     }
-    const path_env_sep = (0, platform_1.get_platform_path_env_separator)(platform);
-    process.env.PATH = ninja_directory + path_env_sep + process.env.PATH;
+    process.env.PATH = ninja_directory + path.delimiter + process.env.PATH;
 }
 exports.configure_ninja_build_tool = configure_ninja_build_tool;
 
@@ -303,7 +583,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
     return result;
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.get_platform_root_directory = exports.get_platform_path_env_separator = exports.get_sdl_build_platform = exports.SdlBuildPlatform = void 0;
+exports.get_platform_root_directory = exports.get_sdl_build_platform = exports.SdlBuildPlatform = void 0;
 const os = __importStar(__nccwpck_require__(2037));
 const core = __importStar(__nccwpck_require__(2186));
 const util_1 = __nccwpck_require__(9731);
@@ -325,17 +605,6 @@ function get_sdl_build_platform() {
     throw new util_1.SetupSdlError("Unsupported build platform");
 }
 exports.get_sdl_build_platform = get_sdl_build_platform;
-function get_platform_path_env_separator(platform) {
-    switch (platform) {
-        case SdlBuildPlatform.Windows:
-            return ";";
-        case SdlBuildPlatform.Macos:
-        case SdlBuildPlatform.Linux:
-        default:
-            return ":";
-    }
-}
-exports.get_platform_path_env_separator = get_platform_path_env_separator;
 function get_platform_root_directory(platform) {
     const root = core.getInput("root");
     if (root) {
diff --git a/src/main.ts b/src/main.ts
index 9b863567a17e..46bb4bfcb92d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,6 +4,7 @@ import * as fs from "fs";
 import * as core from "@actions/core";
 
 import { SDL_GIT_URL } from "./constants";
+import { setup_vc_environment } from "./msvc";
 import { configure_ninja_build_tool } from "./ninja";
 import { SetupSdlError } from "./util";
 
@@ -16,6 +17,7 @@ import {
 import {
   get_sdl_build_platform,
   get_platform_root_directory,
+  SdlBuildPlatform,
 } from "./platform";
 
 async function convert_git_branch_tag_to_hash(
@@ -158,6 +160,12 @@ async function run() {
     });
   }
 
+  if (SDL_BUILD_PLATFORM == SdlBuildPlatform.Windows) {
+    await core.group(`Configuring VS environment`, async () => {
+      setup_vc_environment();
+    });
+  }
+
   const source_dir = `${SETUP_SDL_ROOT}/src`;
   const build_dir = `${SETUP_SDL_ROOT}/build`;
   const install_dir = `${SETUP_SDL_ROOT}`;
diff --git a/src/msvc.ts b/src/msvc.ts
new file mode 100644
index 000000000000..0e00fd5fed29
--- /dev/null
+++ b/src/msvc.ts
@@ -0,0 +1,294 @@
+// Copyright 2019 ilammy
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import * as child_process from "child_process";
+import * as fs from "fs";
+import * as path from "path";
+import * as process from "process";
+
+import * as core from "@actions/core";
+
+import { SetupSdlError } from "./util";
+
+const PROGRAM_FILES_X86 = process.env["ProgramFiles(x86)"];
+const PROGRAM_FILES = [
+  process.env["ProgramFiles(x86)"],
+  process.env["ProgramFiles"],
+];
+
+const EDITIONS = ["Enterprise", "Professional", "Community"];
+const YEARS = ["2022", "2019", "2017"];
+
+const VsYearVersion: { [key: string]: string } = {
+  "2022": "17.0",
+  "2019": "16.0",
+  "2017": "15.0",
+  "2015": "14.0",
+  "2013": "12.0",
+};
+
+const VSWHERE_PATH = `${PROGRAM_FILES_X86}\\Microsoft Visual Studio\\Installer`;
+
+function vsversion_to_versionnumber(vsversion: string): string {
+  if (Object.values(VsYearVersion).includes(vsversion)) {
+    return vsversion;
+  } else {
+    if (vsversion in VsYearVersion) {
+      return VsYearVersion[vsversion];
+    }
+  }
+  return vsversion;
+}
+
+function vsversion_to_year(vsversion: string): string {
+  if (Object.keys(VsYearVersion).includes(vsversion)) {
+    return vsversion;
+  } else {
+    for (const [year, ver] of Object.entries(VsYearVersion)) {
+      if (ver === vsversion) {
+        return year;
+      }
+    }
+  }
+  return vsversion;
+}
+
+function findWithVswhere(
+  pattern: string,
+  version_pattern: string
+): string | null {
+  try {
+    const installationPath = child_process
+      .execSync(
+        `vswhere -products * ${version_pattern} -prerelease -property installationPath`
+      )
+      .toString()
+      .trim();
+    return `${installationPath}\\\\${pattern}`;
+  } catch (e) {
+    core.warning(`vswhere failed: ${e}`);
+  }
+  return null;
+}
+
+function findVcvarsall(vsversion: string): string {
+  const vsversion_number = vsversion_to_versionnumber(vsversion);
+  let version_pattern;
+  if (vsversion_number) {
+    const upper_bound = vsversion_number.split(".")[0] + ".9";
+    version_pattern = `-version "${vsversion_number},${upper_bound}"`;
+  } else {
+    version_pattern = "-latest";
+  }
+
+  // If vswhere is available, ask it about the location of the latest Visual Studio.
+  let path = findWithVswhere(
+    "VC\\Auxiliary\\Build\\vcvarsall.bat",
+    version_pattern
+  );
+  if (path && fs.existsSync(path)) {
+    core.info(`Found with vswhere: ${path}`);
+    return path;
+  }
+  core.info("Not found with vswhere");
+
+  // If that does not work, try the standard installation locations,
+  // starting with the latest and moving to the oldest.
+  const years = vsversion ? [vsversion_to_year(vsversion)] : YEARS;
+  for (const prog_files of PROGRAM_FILES) {
+    for (const ver of years) {
+      for (const ed of EDITIONS) {
+        path = `${prog_files}\\Microsoft Visual Studio\\${ver}\\${ed}\\VC\\Auxiliary\\Build\\vcvarsall.bat`;
+        core.info(`Trying standard location: ${path}`);
+        if (fs.existsSync(path)) {
+          core.info(`Found standard location: ${path}`);
+          return path;
+        }
+      }
+    }
+  }
+  core.info("Not found in standard locations");
+
+  // Special case for Visual Studio 2015 (and maybe earlier), try it out too.
+  path = `${PROGRAM_FILES_X86}\\Microsoft Visual C++ Build Tools\\vcbuildtools.bat`;
+  if (fs.existsSync(path)) {
+    core.info(`Found VS 2015: ${path}`);
+    return path;
+  }
+  core.info(`Not found in VS 2015 location: ${path}`);
+
+  throw new SetupSdlError("Microsoft Visual Studio not found");
+}
+
+function isPathVariable(name: string): boolean {
+  const pathLikeVariables = ["PATH", "INCLUDE", "LIB", "LIBPATH"];
+  return pathLikeVariables.indexOf(name.toUpperCase()) != -1;
+}
+
+function filterPathValue(path: string): string {
+  const paths = path.split(";");
+  // Remove duplicates by keeping the first occurrence and preserving order.
+  // This keeps path shadowing working as intended.
+  function unique(value: string, index: number, self: string[]): boolean {
+    return self.indexOf(value) === index;
+  }
+  return paths.filter(unique).join(";");
+}
+
+/** See https://github.com/ilammy/msvc-dev-cmd#inputs */
+function setupMSVCDevCmd(
+  arch: string,
+  sdk: string,
+  toolset: boolean,
+  uwp: boolean,
+  spectre: boolean,
+  vsversion: string
+) {
+  if (process.platform != "win32") {
+    core.info("This is not a Windows virtual environment, bye!");
+    return;
+  }
+
+  // Add standard location of "vswhere" to PATH, in case it"s not there.
+  process.env.PATH += path.delimiter + VSWHERE_PATH;
+
+  // There are all sorts of way the architectures are called. In addition to
+  // values supported by Microsoft Visual C++, recognize some common aliases.
+  const arch_aliases: { [key: string]: string } = {
+    win32: "x86",
+    win64: "x64",
+    x86_64: "x64",
+    "x86-64": "x64",
+  };
+  // Ignore case when matching as that"s what humans expect.
+  if (arch.toLowerCase() in arch_aliases) {
+    arch = arch_aliases[arch.toLowerCase()];
+  }
+
+  // Due to the way Microsoft Visual C++ is configured, we have to resort to the following hack:
+  // Call the configuration batch file and then output *all* the environment variables.
+
+  const args = [arch];
+  if (uwp) {
+    args.push("uwp");
+  }
+  if (sdk) {
+    args.push(sdk);
+  }
+  if (toolset) {
+    args.push(`-vcvars_ver=${toolset}`);
+  }
+  if (spectre) {
+    args.push("-vcvars_spectre_libs=spectre");
+  }
+
+  const vcvars = `"${findVcvarsall(vsversion)}" ${args.join(" ")}`;
+  core.debug(`vcvars command-line: ${vcvars}`);
+
+  const cmd_output_string = child_process
+    .execSync(`set && cls && ${vcvars} && cls && set`, { shell: "cmd" })
+    .toString();
+  const cmd_output_parts = cmd_output_string.split("\f");
+
+  const old_environment = cmd_output_parts[0].split("\r\n");
+  const vcvars_output = cmd_output_parts[1].split("\r\n");
+  const new_environment = cmd_output_parts[2].split("\r\n");
+
+  // If vsvars.bat is given an incorrect command line, it will print out
+  // an error and *still* exit successfully. Parse out errors from output
+  // which don"t look like environment variables, and fail if appropriate.
+  const error_messages = vcvars_output.filter((line) => {
+    if (line.match(/^\[ERROR.*\]/)) {
+      // Don"t print this particular line which will be confusing in output.
+      if (!line.match(/Error in script usage. The correct usage is:$/)) {
+        return true;
+      }
+    }
+    return false;
+  });
+  if (error_messages.length > 0) {
+    throw new Error(
+      "invalid parameters" + "\r\n" + error_messages.join("\r\n")
+    );
+  }
+
+  const result_vcvars: { [key: string]: string } = {};
+
+  // Convert old environment lines into a dictionary for easier lookup.
+  const old_env_vars: { [key: string]: string } = {};
+  for (const string of old_environment) {
+    const [name, value] = string.split("=");
+    old_env_vars[name] = value;
+  }
+
+  // Now look at the new environment and export everything that changed.
+  // These are the variables set by vsvars.bat. Also export everything
+  // that was not there during the first sweep: those are new variables.
+  core.startGroup("Environment variables");
+  for (const string of new_environment) {
+    const [key, vcvars_value] = string.split("=");
+    // vsvars.bat likes to print some fluff at the beginning.
+    // Skip lines that don"t look like environment variables.
+    if (!vcvars_value) {
+      continue;
+    }
+    const old_value = old_env_vars[key];
+    // For new variables "old_value === undefined".
+    if (vcvars_value !== old_value) {
+      let filtered_value = vcvars_value;
+      core.info(`Setting ${key}`);
+      // Special case for a bunch of PATH-like variables: vcvarsall.bat
+      // just prepends its stuff without checking if its already there.
+      // This makes repeated invocations of this action fail after some
+      // point, when the environment variable overflows. Avoid that.
+      if (isPathVariable(key)) {
+        filtered_value = filterPathValue(vcvars_value);
+      }
+
+      result_vcvars[key] = filtered_value;
+    }
+  }
+  core.endGroup();
+
+  core.info("Configured Developer Command Prompt");
+
+  return result_vcvars;
+}
+
+export function setup_vc_environment() {
+  const arch = core.getInput("vc_arch");
+  const sdk = core.getInput("vc_sdk");
+  const toolset = core.getBooleanInput("vc_toolset");
+  const uwp = core.getBooleanInput("vc_uwp");
+  const spectre = core.getBooleanInput("vc_spectre");
+  const vsversion = core.getInput("vc_vsversion");
+
+  const msvc_env_vars = setupMSVCDevCmd(
+    arch,
+    sdk,
+    toolset,
+    uwp,
+    spectre,
+    vsversion
+  );
+
+  for (const key in msvc_env_vars) {
+    process.env[key] = msvc_env_vars[key];
+  }
+}
diff --git a/src/ninja.ts b/src/ninja.ts
index 49f18672ffd7..3a89ae1cb404 100644
--- a/src/ninja.ts
+++ b/src/ninja.ts
@@ -1,15 +1,12 @@
 import * as fs from "fs";
+import * as path from "path";
 import * as process from "process";
 
 import * as core from "@actions/core";
 import * as tc from "@actions/tool-cache";
 
 import { NINJA_VERSION } from "./constants";
-import {
-  SdlBuildPlatform,
-  get_platform_root_directory,
-  get_platform_path_env_separator,
-} from "./platform";
+import { SdlBuildPlatform, get_platform_root_directory } from "./platform";
 
 export function get_ninja_download_url(
   platform: SdlBuildPlatform,
@@ -53,7 +50,5 @@ export async function configure_ninja_build_tool(platform: SdlBuildPlatform) {
     );
   }
 
-  const path_env_sep = get_platform_path_env_separator(platform);
-
-  process.env.PATH = ninja_directory + path_env_sep + process.env.PATH;
+  process.env.PATH = ninja_directory + path.delimiter + process.env.PATH;
 }
diff --git a/src/platform.ts b/src/platform.ts
index 88813d89ceb7..9a2dc2720901 100644
--- a/src/platform.ts
+++ b/src/platform.ts
@@ -21,19 +21,6 @@ export function get_sdl_build_platform(): SdlBuildPlatform {
   throw new SetupSdlError("Unsupported build platform");
 }
 
-export function get_platform_path_env_separator(
-  platform: SdlBuildPlatform
-): string {
-  switch (platform) {
-    case SdlBuildPlatform.Windows:
-      return ";";
-    case SdlBuildPlatform.Macos:
-    case SdlBuildPlatform.Linux:
-    default:
-      return ":";
-  }
-}
-
 export function get_platform_root_directory(
   platform: SdlBuildPlatform
 ): string {