--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki
-- @file        find_vstudio.lua
--

-- imports
import("core.base.option")
import("core.base.semver")
import("core.base.hashset")
import("core.project.config")
import("lib.detect.find_file")
import("lib.detect.find_tool")
import("lib.detect.find_directory")
import("core.cache.global_detectcache")

-- init vsvers
local vsvers = {
    ["18.0"] = "2026"
,   ["17.0"] = "2022"
,   ["16.0"] = "2019"
,   ["15.0"] = "2017"
,   ["14.0"] = "2015"
,   ["12.0"] = "2013"
,   ["11.0"] = "2012"
,   ["10.0"] = "2010"
,   ["9.0"]  = "2008"
,   ["8.0"]  = "2005"
,   ["7.1"]  = "2003"
,   ["7.0"]  = "7.0"
,   ["6.0"]  = "6.0"
,   ["5.0"]  = "5.0"
,   ["4.2"]  = "4.2"
}

-- init vsenvs
local vsenvs = {
    ["18.0"] = "VS180COMNTOOLS"
,   ["17.0"] = "VS170COMNTOOLS"
,   ["16.0"] = "VS160COMNTOOLS"
,   ["15.0"] = "VS150COMNTOOLS"
,   ["14.0"] = "VS140COMNTOOLS"
,   ["12.0"] = "VS120COMNTOOLS"
,   ["11.0"] = "VS110COMNTOOLS"
,   ["10.0"] = "VS100COMNTOOLS"
,   ["9.0"]  = "VS90COMNTOOLS"
,   ["8.0"]  = "VS80COMNTOOLS"
,   ["7.1"]  = "VS71COMNTOOLS"
,   ["7.0"]  = "VS70COMNTOOLS"
,   ["6.0"]  = "VS60COMNTOOLS"
,   ["5.0"]  = "VS50COMNTOOLS"
,   ["4.2"]  = "VS42COMNTOOLS"
}

-- the original environment variables
local _env_orgs = {}

-- get all known Visual Studio environment variables
function get_vcvars()
    local vcvars = {
        "PATH",
        "LIB",
        "LIBPATH",
        "INCLUDE",
        "DevEnvdir",
        "VSInstallDir",
        "VCInstallDir",
        "WindowsSdkDir",
        "WindowsLibPath",
        "WindowsSDKVersion",
        "WindowsSdkBinPath",
        "WindowsSdkVerBinPath",
        "ExtensionSdkDir",
        "UniversalCRTSdkDir",
        "UCRTVersion",
        "VCToolsVersion",
        "VCIDEInstallDir",
        "VCToolsInstallDir",
        "VCToolsRedistDir",
        "VisualStudioVersion",
        "VSCMD_VER",
        "VSCMD_ARG_app_plat",
        "VSCMD_ARG_HOST_ARCH",
        "VSCMD_ARG_TGT_ARCH"}

    local realvcvars = vcvars
    for _, v in pairs(vsenvs) do
        table.insert(realvcvars, v)
    end
    return realvcvars
end

function find_build_tools(opt)
    opt = opt or {}

    local sdkdir = opt.sdkdir
    if not sdkdir or not os.isdir(sdkdir) then
        return
    end

    local variables = {}
    local VCInstallDir = path.join(sdkdir, "VC")
    local VCToolsVersion = opt.vs_toolset
    if not VCToolsVersion or not os.isdir(path.join(VCInstallDir, "Tools/MSVC", VCToolsVersion)) then
        -- https://github.com/xmake-io/xmake/issues/6159
        local latest_toolset
        for _, dir in ipairs(os.dirs(path.join(sdkdir, "VC/Tools/MSVC/*"))) do
            local toolset = path.filename(dir)
            if not latest_toolset or semver.compare(toolset, latest_toolset) > 0 then
                latest_toolset = toolset
            end
        end
        if latest_toolset then
            VCToolsVersion = latest_toolset
        else
            return
        end
    end
    variables.VCInstallDir = VCInstallDir
    variables.VCToolsVersion = VCToolsVersion
    variables.VCToolsInstallDir = path.join(VCInstallDir, "Tools/MSVC", VCToolsVersion)

    local WindowsSDKVersion
    local vs_sdkver = opt.vs_sdkver
    if vs_sdkver and os.isdir(path.join(sdkdir, "Windows Kits/10/Lib", vs_sdkver)) then
        WindowsSDKVersion = vs_sdkver
    else
        local dir = find_directory("10*", path.join(sdkdir, "Windows Kits/10/Lib"))
        if dir then
            WindowsSDKVersion = path.filename(dir)
        else
            return
        end
    end
    variables.WindowsSDKVersion = WindowsSDKVersion
    variables.WindowsSdkDir = path.join(sdkdir, "Windows Kits/10")
    variables.WindowsSdkBinPath = path.join(variables.WindowsSdkDir, "bin")
    variables.WindowsSdkVerBinPath = path.join(variables.WindowsSdkBinPath, WindowsSDKVersion)
    variables.ExtensionSdkDir = path.join(variables.WindowsSdkDir, "ExtensionSdkDir")
    variables.UCRTVersion = WindowsSDKVersion
    variables.UniversalCRTSdkDir = variables.WindowsSdkDir

    local includedirs = {
        path.join(variables.VCToolsInstallDir, "include"),
        path.join(variables.VCToolsInstallDir, "atlmfc", "include"),
        path.join(variables.WindowsSdkDir, "Include", WindowsSDKVersion, "ucrt"),
        path.join(variables.WindowsSdkDir, "Include", WindowsSDKVersion, "shared"),
        path.join(variables.WindowsSdkDir, "Include", WindowsSDKVersion, "um"),
        path.join(variables.WindowsSdkDir, "Include", WindowsSDKVersion, "winrt"),
        path.join(variables.WindowsSdkDir, "Include", WindowsSDKVersion, "cppwinrt"),
    }

    local linkdirs = {
        path.join(variables.VCToolsInstallDir, "lib"),
        path.join(variables.VCToolsInstallDir, "atlmfc", "lib"),
        path.join(variables.WindowsSdkDir, "Lib", WindowsSDKVersion, "ucrt"),
        path.join(variables.WindowsSdkDir, "Lib", WindowsSDKVersion, "um"),
        path.join(variables.WindowsSdkDir, "Lib", WindowsSDKVersion, "km"),
    }

    local archs = {
        "x86",
        "x64",
        "arm",
        "arm64",
    }

    local vcvarsall = {}
    for _, target_arch in ipairs(archs) do
        local lib = {}
        for _, lib_dir in ipairs(linkdirs) do
            local dir = path.join(lib_dir, target_arch)
            if os.isdir(dir) then
                table.insert(lib, dir)
            end
        end

        if #lib ~= 0 then
            local vcvars = {
                BUILD_TOOLS_ROOT = sdkdir,
                VSInstallDir = sdkdir,

                -- vs runs in a windows ctx, so the envsep is always ";"
                INCLUDE = path.joinenv(includedirs, ';'),
                LIB = path.joinenv(lib, ';'),

                VSCMD_ARG_HOST_ARCH = "x64",

                VCInstallDir = variables.VCInstallDir,
                VCToolsVersion = variables.VCToolsVersion,
                VCToolsInstallDir = variables.VCToolsInstallDir,

                WindowsSDKVersion = variables.WindowsSDKVersion,
                WindowsSdkDir = variables.WindowsSdkDir,
                WindowsSdkBinPath = variables.WindowsSdkBinPath,
                WindowsSdkVerBinPath = variables.WindowsSdkVerBinPath,
                ExtensionSdkDir = variables.ExtensionSdkDir,
                UCRTVersion = variables.UCRTVersion,
                UniversalCRTSdkDir = variables.UniversalCRTSdkDir,
            }

            local build_tools_bin = {}
            local host_dir = "Host" .. vcvars.VSCMD_ARG_HOST_ARCH
            if is_host("windows") then
                table.insert(build_tools_bin, path.join(vcvars.VCToolsInstallDir, "bin", host_dir, target_arch))
                table.insert(build_tools_bin, path.join(vcvars.WindowsSdkDir, "bin", WindowsSDKVersion))
                table.insert(build_tools_bin, path.join(vcvars.WindowsSdkDir, "bin", WindowsSDKVersion, "ucrt"))
            elseif is_host("linux") then
                -- for msvc-wine
                table.insert(build_tools_bin, path.join(sdkdir, "bin", target_arch))
            end

            vcvars.VSCMD_ARG_TGT_ARCH = target_arch
            vcvars.BUILD_TOOLS_BIN = path.joinenv(build_tools_bin)

            local PATH = build_tools_bin
            table.join2(PATH, path.splitenv(os.getenv("PATH")))
            vcvars.PATH = path.joinenv(PATH)

            vcvarsall[target_arch] = vcvars
        end
    end

    return vcvarsall
end

-- load vcvarsall environment variables
function _load_vcvarsall_impl(vcvarsall, vsver, arch, opt)
    opt = opt or {}

    -- is VsDevCmd.bat?
    local is_vsdevcmd = path.basename(vcvarsall):lower() == "vsdevcmd"

    -- make the genvcvars.bat
    local genvcvars_bat = os.tmpfile() .. "_genvcvars.bat"
    local file = io.open(genvcvars_bat, "w")
    file:print("@echo off")
    -- @note we need to get utf8 output from cmd.exe
    -- because some %PATH% and other envs maybe contains unicode characters
	if winos.version():gt("winxp") then
    	file:print("chcp 65001")
	end
    -- fix error caused by the new vsDevCmd.bat of vs2019
    -- @see https://github.com/xmake-io/xmake/issues/549
    if vsver and tonumber(vsver) >= 16 then
        file:print("set VSCMD_SKIP_SENDTELEMETRY=yes")
    end
    local host_arch = os.arch()
    if is_vsdevcmd then
        if vsver and tonumber(vsver) >= 16 then
            if opt.toolset then
                file:print("call \"%s\" -host_arch=%s -arch=%s -winsdk=%s -vcvars_ver=%s > nul", vcvarsall, host_arch, arch, opt.sdkver or "", opt.toolset or "")
            else
                file:print("call \"%s\" -host_arch=%s -arch=%s -winsdk=%s > nul", vcvarsall, host_arch, arch, opt.sdkver or "")
            end
        else
            if opt.toolset then
                file:print("call \"%s\" -arch=%s -winsdk=%s -vcvars_ver=%s > nul", vcvarsall, arch, opt.sdkver or "", opt.toolset or "")
            else
                file:print("call \"%s\" -arch=%s -winsdk=%s > nul", vcvarsall, arch, opt.sdkver or "")
            end
        end
    else
        -- @see https://github.com/xmake-io/xmake/issues/5077
        if vsver and tonumber(vsver) >= 16 and host_arch ~= arch then
            if host_arch == "x64" then
                host_arch = "amd64"
            end
            arch = host_arch .. "_" .. arch
        end
        if opt.toolset then
            file:print("call \"%s\" %s %s -vcvars_ver=%s > nul", vcvarsall, arch, opt.sdkver or "", opt.toolset or "")
        else
            file:print("call \"%s\" %s %s > nul", vcvarsall, arch, opt.sdkver or "")
        end
    end
    for idx, var in ipairs(get_vcvars()) do
        file:print("echo " .. var .. " = %%" .. var .. "%%")
    end
    file:close()

    -- run genvcvars.bat
    -- @note we use iorunv here so the bat path is not split on whitespace by os.argv,
    -- which breaks detection when the temp file lives under a path with spaces.
    local outdata, errdata = try {function () return os.iorunv(genvcvars_bat) end}
    if errdata and #errdata > 0 and option.get("verbose") and option.get("diagnosis") then
        cprint("${color.warning}checkinfo: ${clear dim}get vcvars error: %s", errdata)
    end
    if not outdata then
        return
    end

    -- load all envirnoment variables
    local variables = {}
    for _, line in ipairs(outdata:split("\n")) do
        local p = line:find('=', 1, true)
        if p then
            local name = line:sub(1, p - 1):trim()
            if #name > 0 then
                local value = line:sub(p + 1):trim()
                if #value > 0 then
                    variables[name] = value
                end
            end
        end
    end

    -- check if the environment variables are truncated
    _check_vcvarsall_env(variables)
    if not variables.PATH then
        return
    end

    -- fix WindowsSDKVersion
    local WindowsSDKVersion = variables.WindowsSDKVersion
    if WindowsSDKVersion then
        WindowsSDKVersion = WindowsSDKVersion:gsub("\\", ""):trim()
        if WindowsSDKVersion ~= "" then
            variables.WindowsSDKVersion = WindowsSDKVersion
        end
    else
        -- sometimes the variable `WindowsSDKVersion` is not available
        -- then parse it from `WindowsSdkBinPath`, such as: `C:\\Program Files (x86)\\Windows Kits\\8.1\\bin`
        local WindowsSdkBinPath = variables.WindowsSdkBinPath
        if WindowsSdkBinPath then
            WindowsSDKVersion = string.match(WindowsSdkBinPath, "\\(%d+%.%d+)\\bin$")
            if WindowsSDKVersion then
                variables.WindowsSDKVersion = WindowsSDKVersion
            end
        end
    end

    -- fix UCRTVersion
    --
    -- @note vcvarsall.bat maybe detect error if install WDK and SDK at same time (multi-sdk version exists in include directory).
    --
    local UCRTVersion = variables.UCRTVersion
    if UCRTVersion and WindowsSDKVersion and UCRTVersion ~= WindowsSDKVersion and WindowsSDKVersion ~= "" then
        local lib = variables.LIB
        if lib then
            lib = lib:gsub(UCRTVersion, WindowsSDKVersion)
            variables.LIB = lib
        end
        local include = variables.INCLUDE
        if include then
            include = include:gsub(UCRTVersion, WindowsSDKVersion)
            variables.INCLUDE = include
        end
        UCRTVersion = WindowsSDKVersion
        variables.UCRTVersion = UCRTVersion
    end
    return variables
end

-- strip toolset version, e.g. 14.16.27023 -> 14.16
function _strip_toolset_ver(vs_toolset)
    local version = semver.new(vs_toolset)
    if version then
        return version:major() .. "." .. version:minor()
    end
    return vs_toolset
end

-- check if the environment variables are truncated
-- https://github.com/xmake-io/xmake/issues/7281
function _check_vcvarsall_env(vars)
    if not option.get("diagnosis") then
        return
    end
    local check_vars = {"PATH", "INCLUDE", "LIB", "LIBPATH"}
    for _, name in ipairs(check_vars) do
        local value_org = _env_orgs[name]
        if value_org == nil then
            local value_str = os.getenv(name)
            if value_str then
                _env_orgs[name] = path.splitenv(value_str)
            else
                _env_orgs[name] = false
            end
            value_org = _env_orgs[name]
        end
        local value_new = vars[name]
        if value_org and value_new and #value_org > 0 then
            local values_new = hashset.from(path.splitenv(value_new))
            for _, p in ipairs(value_org) do
                local p = p
                if not values_new:has(p) then
                    if #p > 256 then
                        p = p:sub(1, 256) .. "..."
                    end
                    wprint("%%%s%% is too long and truncated, msvc detection may fail, please clear some unused variables!\n  > %s", name, p)
                    break
                end
            end
        end
    end
end

function _load_vcvarsall(vcvarsall, vsver, arch, opt)
    opt = opt or {}
    local vs_toolset = opt.toolset or opt.vcvars_ver
    if vs_toolset then
        opt.toolset = _strip_toolset_ver(vs_toolset)
    end
    local result = _load_vcvarsall_impl(vcvarsall, vsver, arch, opt)
    if result and not vs_toolset then
        -- if no vs toolset version is specified, we default to the latest version.
        -- https://github.com/xmake-io/xmake/issues/6159
        local latest_toolset
        local VCToolsVersion = result.VCToolsVersion
        local VCInstallDir = result.VCInstallDir
        if VCToolsVersion and VCInstallDir then
            for _, dir in ipairs(os.dirs(path.join(VCInstallDir, "Tools/MSVC/*"))) do
                local toolset = path.filename(dir)
                if not latest_toolset or semver.compare(toolset, latest_toolset) > 0 then
                    latest_toolset = toolset
                end
            end
        end
        if latest_toolset and VCToolsVersion and semver.compare(latest_toolset, VCToolsVersion) > 0 then
            opt.toolset = _strip_toolset_ver(latest_toolset)
            result = _load_vcvarsall_impl(vcvarsall, vsver, arch, opt)
        end
    end
    return result
end

-- find vstudio for msvc
function _find_vstudio(opt)
    opt = opt or {}

    -- clear local cache of environment variables
    _env_orgs = {}

    -- find the single current MSVC/VS from environment variables
    local VCInstallDir = os.getenv("VCInstallDir")
    if VCInstallDir and (VCInstallDir ~= "") then
        local VisualStudioVersion = os.getenv("VisualStudioVersion")
        if not VisualStudioVersion or (VisualStudioVersion == "") then

            -- heuristic for VisualStudioVersion value (early MSVC/VS versions don't set VisualStudioVersion)
            local VSInstallDir = os.getenv("VSInstallDir") or ""
            VisualStudioVersion = VSInstallDir:match('(%d+[.]?%d*)\\?%s*$')
            if not VisualStudioVersion then VisualStudioVersion = VCInstallDir:match('(%d+[.]?%d*)\\VC\\?%s*$') end
            if not VisualStudioVersion then VisualStudioVersion = "0" end
            if not VisualStudioVersion:match('[.]') then VisualStudioVersion = VisualStudioVersion .. '.0' end

            -- find highest known version which is less than or equal to VisualStudioVersion
            if not vsvers[VisualStudioVersion] then
                local versions = {}
                local count = 0
                for k in pairs(vsvers) do
                    table.insert(versions, tonumber(k))
                    count = count + 1
                end
                table.sort(versions)
                local i = 0
                local v = tonumber(VisualStudioVersion)
                while ((i < count) and (versions[i + 1] <= v)) do
                    i = i + 1
                end
                VisualStudioVersion = versions[i] or "0"
            end
        end

        -- find vcvarsall.bat or vcvars32.bat
        local paths =
        {
            path.join(VCInstallDir, "Auxiliary", "Build"),
            path.join(VCInstallDir, "bin"),
            VCInstallDir
        }
        local vcvarsall = find_file("vcvarsall.bat", paths) or find_file("vcvars32.bat", paths)
        if vcvarsall and os.isfile(vcvarsall) and vsvers[VisualStudioVersion] then

            -- load vcvarsall
            local vcvarsall_x86     = _load_vcvarsall(vcvarsall, VisualStudioVersion, "x86", opt)
            local vcvarsall_x64     = _load_vcvarsall(vcvarsall, VisualStudioVersion, "x64", opt)
            local vcvarsall_arm     = _load_vcvarsall(vcvarsall, VisualStudioVersion, "arm", opt)
            local vcvarsall_arm64   = _load_vcvarsall(vcvarsall, VisualStudioVersion, "arm64", opt)
            local vcvarsall_arm64ec = vcvarsall_arm64

            -- save results
            local results = {}
            results[vsvers[VisualStudioVersion]] = {
                version = VisualStudioVersion,
                vcvarsall_bat = vcvarsall,
                vcvarsall = {
                    x86 = vcvarsall_x86,
                    x64 = vcvarsall_x64,
                    arm = vcvarsall_arm,
                    arm64 = vcvarsall_arm64,
                    arm64ec = vcvarsall_arm64ec
                }
            }
            return results
        end
    end

    -- find vswhere
    local vswhere = find_tool("vswhere")

    -- sort vs versions
    local order_vsvers = table.keys(vsvers)
    table.sort(order_vsvers, function (a, b) return tonumber(a) > tonumber(b) end)

    -- find vs2017 -> vs4.2
    local results = {}
    for _, version in ipairs(order_vsvers) do
        local version = version

        -- find VC install path (and aux build path) using `vswhere` (for version >= 15.0)
        -- * version > 15.0 eschews registry entries; but `vswhere` (included with version >= 15.2) can be used to find VC install path
        -- ref: https://github.com/Microsoft/vswhere/blob/master/README.md @@ https://archive.is/mEmdu
        local vswhere_VCAuxiliaryBuildDir = {}
        local vswhere_Common7ToolsDir = {}
        if (tonumber(version) >= 15) and vswhere then
            local vswhere_vrange = format("%s,%s)", version, (version + 1))
            -- build tools: https://github.com/microsoft/vswhere/issues/22 @@ https://aka.ms/vs/workloads
            local result = os.iorunv(vswhere.program, {"-products", "*", "-prerelease", "-property", "installationpath", "-version", vswhere_vrange})
            if result then
                for _, vc_path in ipairs(result:split("\n")) do
                    table.insert(vswhere_VCAuxiliaryBuildDir, path.join(vc_path:trim(), "VC", "Auxiliary", "Build"))
                    table.insert(vswhere_Common7ToolsDir, path.join(vc_path:trim(), "Common7", "Tools"))
                end
            end
        end

        -- init paths
        local paths = {
            format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\VC", version),
            format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\VC7\\bin", version),
            format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\VC", version),
            format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\VC\\Auxiliary\\Build", version),
            format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\VC\\Auxiliary\\Build", version)
        }
        if vsenvs[version] then
            table.insert(paths, format("$(env %s)\\..\\..\\VC", vsenvs[version]))
        end

        if vswhere_VCAuxiliaryBuildDir then
            for _, vc_path in ipairs(vswhere_VCAuxiliaryBuildDir) do
                if os.isdir(vc_path) then
                    table.insert(paths, 1, vc_path)
                end
            end
        end
        if version == "6.0" and os.arch() == "x64" then
            table.insert(paths, "$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\DevStudio\\6.0\\Products\\Microsoft Visual C++;ProductDir)\\Bin")
            table.insert(paths, "$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\6.0\\Setup\\Microsoft Visual C++;ProductDir)\\Bin")
        end

        -- find vcvarsall.bat, vcvars32.bat for vs7.1
        local vcvarsall = find_file("vcvarsall.bat", paths) or find_file("vcvars32.bat", paths)
        if not vcvarsall then
            -- find vs from some logical drives paths
            paths = {}
            local logical_drives = winos.logical_drives()
            -- we attempt to find vs from wdk directory
            -- wdk: E:\Program Files\Windows Kits\10
            -- vcvarsall: E:\Program Files\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build
            local wdk = config.get("wdk")
            if wdk and os.isdir(wdk) then
                local p = wdk:find("Program Files")
                if p then
                    table.insert(logical_drives, wdk:sub(1, p - 1))
                end
            end
            for _, logical_drive in ipairs(logical_drives) do
                if os.isdir(path.join(logical_drive, "Program Files (x86)")) then
                    table.insert(paths, path.join(logical_drive, "Program Files (x86)", "Microsoft Visual Studio", vsvers[version], "*", "VC", "Auxiliary", "Build"))
                    table.insert(paths, path.join(logical_drive, "Program Files (x86)", "Microsoft Visual Studio " .. version, "VC"))
                end
                table.insert(paths, path.join(logical_drive, "Program Files", "Microsoft Visual Studio", vsvers[version], "*", "VC", "Auxiliary", "Build"))
                table.insert(paths, path.join(logical_drive, "Program Files", "Microsoft Visual Studio " .. version, "VC"))
                if version == "6.0" then
                    table.insert(paths, path.join(logical_drive, "Program Files", "Microsoft Visual Studio", "VC98", "Bin"))
                    table.insert(paths, path.join(logical_drive, "Program Files (x86)", "Microsoft Visual Studio", "VC98", "Bin"))
                end
            end
            vcvarsall = find_file("vcvarsall.bat", paths) or find_file("vcvars32.bat", paths)
        end
        if not vcvarsall then
            local paths = {
                format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\Common7\\Tools", version),
                format("$(reg HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7;%s)\\Common7\\Tools", version)
            }
            if vswhere_Common7ToolsDir then
                for _, vc_path in ipairs(vswhere_Common7ToolsDir) do
                    if os.isdir(vc_path) then
                        table.insert(paths, 1, vc_path)
                    end
                end
            end
            vcvarsall = find_file("VsDevCmd.bat", paths)
        end
        if vcvarsall then

            -- load vcvarsall
            local vcvarsall_x86     = _load_vcvarsall(vcvarsall, version, "x86", opt)
            local vcvarsall_x64     = _load_vcvarsall(vcvarsall, version, "x64", opt)
            local vcvarsall_arm     = _load_vcvarsall(vcvarsall, version, "arm", opt)
            local vcvarsall_arm64   = _load_vcvarsall(vcvarsall, version, "arm64", opt)
            local vcvarsall_arm64ec = vcvarsall_arm64

            -- save results
            results[vsvers[version]] = {
                version = version,
                vcvarsall_bat = vcvarsall,
                vcvarsall = {
                    x86 = vcvarsall_x86,
                    x64 = vcvarsall_x64,
                    arm = vcvarsall_arm,
                    arm64 = vcvarsall_arm64,
                    arm64ec = vcvarsall_arm64ec
                }
            }
        end
    end
    return results
end

-- get last mtime of msvc
-- @see https://github.com/xmake-io/xmake/issues/3652
function _get_last_mtime_of_msvc(msvc)
    local mtime = -1
    for arch, envs in pairs(msvc.vcvarsall) do
        if envs.PATH then
            local pathenv = path.splitenv(envs.PATH)
            for _, dir in ipairs(pathenv) do
                local cl = path.join(dir, "cl.exe")
                if os.isfile(cl) then
                    local t = os.mtime(cl)
                    if t > mtime then
                        mtime = t
                    end
                end
            end
        end
        local winsdk = envs.WindowsSdkDir
        if winsdk and os.isdir(winsdk) then
            local t = os.mtime(winsdk)
            if t > mtime then
                mtime = t
            end
        end
    end
    return mtime
end

-- get last mtime of vstudio
function _get_last_mtime(vstudio)
    local mtime = -1
    for _, msvc in pairs(vstudio) do
        local vcvarsall_bat = msvc.vcvarsall_bat
        if vcvarsall_bat and os.isfile(vcvarsall_bat) then
            local t = os.mtime(vcvarsall_bat)
            if t > mtime then
                mtime = t
            end
            t = _get_last_mtime_of_msvc(msvc)
            if t > mtime then
                mtime = t
            end
        else
            mtime = -1
            break
        end
    end
    return mtime
end

function _show_vstudio_diagnosis_result(vstudio)
    local found = {}
    for vsver, msvc in pairs(vstudio) do
        if type(msvc) == "table" and msvc.version then
            local toolset
            local vcvarsall = msvc.vcvarsall
            if type(vcvarsall) == "table" then
                for _, envs in pairs(vcvarsall) do
                    if type(envs) == "table" and envs.VCToolsVersion then
                        toolset = _strip_toolset_ver(envs.VCToolsVersion)
                        break
                    end
                end
            end
            if toolset then
                table.insert(found, tostring(vsver) .. "(" .. tostring(msvc.version) .. ", toolset:" .. tostring(toolset) .. ")")
            else
                table.insert(found, tostring(vsver) .. "(" .. tostring(msvc.version) .. ")")
            end
        else
            table.insert(found, tostring(vsver))
        end
    end
    table.sort(found, function (a, b) return a > b end)
    cprint("${dim}detecting vstudio environment ... ${color.success}%s", table.concat(found, ", "))
end

-- find vstudio environment
--
-- @param opt   the options, e.g. {toolset = 14.0, sdkver = "10.0.15063.0"}
--
-- @return      { 2008 = {version = "9.0", vcvarsall = {x86 = {path = .., lib = .., include = ..}}}
--              , 2017 = {version = "15.0", vcvarsall = {x64 = {path = .., lib = ..}}}}
--
function main(opt)
    opt = opt or {}

    -- only for windows
    if not is_host("windows") then
        return
    end

    local key = "vstudio"
    if opt.toolset then
        key = key .. opt.toolset
    end
    if opt.sdkver then
        key = key .. opt.sdkver
    end

    -- attempt to get it from the global cache first
    local vstudio = global_detectcache:get2(key, "msvc")
    if vstudio then
        local mtime = _get_last_mtime(vstudio)
        local mtimeprev = global_detectcache:get2(key, "mtime")
        if mtime and mtimeprev and mtime > 0 and mtimeprev > 0 and mtime == mtimeprev then
            return vstudio
        end
    end

    -- find and cache result
    cprint("${color.warning}Detecting Visual Studio environment (cached); run `xmake global --clean` first if vs updated.")
    vstudio = _find_vstudio(opt)
    if vstudio then
        local mtime = _get_last_mtime(vstudio)
        global_detectcache:set2(key, "msvc", vstudio)
        global_detectcache:set2(key, "mtime", mtime)
        global_detectcache:save()
    end
    if option.get("diagnosis") then
        if vstudio and not table.empty(vstudio) then
            _show_vstudio_diagnosis_result(vstudio)
        else
            cprint("${dim}detecting vstudio environment ...${color.failure}${text.failure}")
        end
    end
    return vstudio
end
