--!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        cmake.lua
--

-- imports
import("core.base.option")
import("core.base.semver")
import("core.base.hashset")
import("core.tool.toolchain")
import("core.project.config")
import("core.project.project")
import("lib.detect.find_file")
import("lib.detect.find_tool")
import("package.tools.ninja")
import("package.tools.msbuild")
import("detect.sdks.find_emsdk")
import("private.utils.toolchain", {alias = "toolchain_utils"})

-- get the number of parallel jobs
function _get_parallel_njobs(opt)
    return opt.jobs or option.get("jobs") or tostring(os.default_njob())
end

-- translate paths
function _translate_paths(paths)
    if is_host("windows") then
        if type(paths) == "string" then
            return (paths:gsub("\\", "/"))
        elseif type(paths) == "table" then
            local result = {}
            for _, p in ipairs(paths) do
                table.insert(result, (p:gsub("\\", "/")))
            end
            return result
        end
    end
    return paths
end

-- translate bin path
function _translate_bin_path(bin_path)
    if is_host("windows") and bin_path then
        bin_path = bin_path:gsub("\\", "/")
        if not os.isfile(bin_path) and
           not bin_path:find(string.ipattern("%.exe$")) and
           not bin_path:find(string.ipattern("%.cmd$")) and
           not bin_path:find(string.ipattern("%.bat$")) then
            bin_path = bin_path .. ".exe"
        end
    end
    return bin_path
end

-- get pkg-config, we need force to find it, because package install environments will be changed
function _get_pkgconfig(package)
    -- meson need fullpath pkgconfig
    -- @see https://github.com/xmake-io/xmake/issues/5474
    local dep = package:dep("pkgconf") or package:dep("pkg-config")
    if dep then
        local suffix = dep:is_plat("windows", "mingw") and ".exe" or ""
        local pkgconf = path.join(dep:installdir("bin"), "pkgconf" .. suffix)
        if os.isfile(pkgconf) then
            return pkgconf
        end
        local pkgconfig = path.join(dep:installdir("bin"), "pkg-config" .. suffix)
        if os.isfile(pkgconfig) then
            return pkgconfig
        end
    end
    if package:is_plat("windows") then
        local pkgconf = find_tool("pkgconf", {force = true})
        if pkgconf then
            return pkgconf.program
        end
    end
    local pkgconfig = find_tool("pkg-config", {force = true})
    if pkgconfig then
        return pkgconfig.program
    end
end

-- is the toolchain compatible with the host?
function _is_toolchain_compatible_with_host(package)
    for _, name in ipairs(package:config("toolchains")) do
        if toolchain_utils.is_compatible_with_host(name) then
            return true
        end
    end
end

-- get msvc
function _get_msvc(package)
    local msvc = package:toolchain("msvc")
    assert(msvc:check(), "vs not found!") -- we need to check vs envs if it has been not checked yet
    return msvc
end

-- get msvc run environments
function _get_msvc_runenvs(package)
    return os.joinenvs(_get_msvc(package):runenvs())
end

-- get cflags from package deps
function _get_cflags_from_packagedeps(package, opt)
    local values
    for _, depname in ipairs(opt.packagedeps) do
        local dep = type(depname) ~= "string" and depname or package:librarydep(depname)
        if dep then
            local fetchinfo = dep:fetch()
            if fetchinfo then
                if values then
                    values = values .. fetchinfo
                else
                    values = fetchinfo
                end
            end
        end
    end
    -- @see https://github.com/xmake-io/xmake-repo/pull/4973#issuecomment-2295890196
    local result = {}
    if values then
        if values.defines then
            table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "define", values.defines))
        end
        if values.includedirs then
            table.join2(result, _translate_paths(toolchain_utils.map_compflags_for_package(package, "cxx", "includedir", values.includedirs)))
        end
        if values.sysincludedirs then
            table.join2(result, _translate_paths(toolchain_utils.map_compflags_for_package(package, "cxx", "sysincludedir", values.sysincludedirs)))
        end
    end
    return result
end

-- get ldflags from package deps
function _get_ldflags_from_packagedeps(package, opt)
    local values
    for _, depname in ipairs(opt.packagedeps) do
        local dep = type(depname) ~= "string" and depname or package:librarydep(depname)
        if dep then
            local fetchinfo = dep:fetch()
            if fetchinfo then
                if values then
                    values = values .. fetchinfo
                else
                    values = fetchinfo
                end
            end
        end
    end
    local result = {}
    if values then
        if values.linkdirs then
            table.join2(result, _translate_paths(toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "linkdir", values.linkdirs)))
        end
        if values.links then
            table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "link", values.links))
        end
        if values.syslinks then
            table.join2(result, _translate_paths(toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "syslink", values.syslinks)))
        end
        if values.frameworks then
            table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "framework", values.frameworks))
        end
    end
    return result
end

-- get cflags
function _get_cflags(package, opt)
    opt = opt or {}
    local result = {}
    if opt.cross then
        table.join2(result, package:build_getenv("cflags"))
        table.join2(result, package:build_getenv("cxflags"))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "c", "define", package:build_getenv("defines")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "c", "includedir", package:build_getenv("includedirs")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "c", "sysincludedir", package:build_getenv("sysincludedirs")))
    end
    table.join2(result, package:config("cflags"))
    table.join2(result, package:config("cxflags"))
    if opt.cflags then
        table.join2(result, opt.cflags)
    end
    if opt.cxflags then
        table.join2(result, opt.cxflags)
    end
    if package:config("lto") then
        table.join2(result, package:_generate_lto_configs("cc").cflags)
    end
    if package:config("asan") then
        table.join2(result, package:_generate_sanitizer_configs("address", "cc").cflags)
    end
    table.join2(result, _get_cflags_from_packagedeps(package, opt))
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end

-- get cxxflags
function _get_cxxflags(package, opt)
    opt = opt or {}
    local result = {}
    if opt.cross then
        table.join2(result, package:build_getenv("cxxflags"))
        table.join2(result, package:build_getenv("cxflags"))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "define", package:build_getenv("defines")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "includedir", package:build_getenv("includedirs")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "cxx", "sysincludedir", package:build_getenv("sysincludedirs")))
    end
    table.join2(result, package:config("cxxflags"))
    table.join2(result, package:config("cxflags"))
    if opt.cxxflags then
        table.join2(result, opt.cxxflags)
    end
    if opt.cxflags then
        table.join2(result, opt.cxflags)
    end
    if package:config("lto") then
        table.join2(result, package:_generate_lto_configs("cxx").cxxflags)
    end
    if package:config("asan") then
        table.join2(result, package:_generate_sanitizer_configs("address", "cxx").cxxflags)
    end
    table.join2(result, _get_cflags_from_packagedeps(package, opt))
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end

-- get asflags
function _get_asflags(package, opt)
    opt = opt or {}
    local result = {}
    if opt.cross then
        table.join2(result, package:build_getenv("asflags"))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "as", "define", package:build_getenv("defines")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "as", "includedir", package:build_getenv("includedirs")))
        table.join2(result, toolchain_utils.map_compflags_for_package(package, "as", "sysincludedir", package:build_getenv("sysincludedirs")))
    end
    table.join2(result, package:config("asflags"))
    if opt.asflags then
        table.join2(result, opt.asflags)
    end
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end

-- get ldflags
function _get_ldflags(package, opt)
    opt = opt or {}
    local result = {}
    if opt.cross then
        table.join2(result, package:build_getenv("ldflags"))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "link", package:build_getenv("links")))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "syslink", package:build_getenv("syslinks")))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "linkdir", package:build_getenv("linkdirs")))
    end
    table.join2(result, package:config("ldflags"))
    if package:config("lto") then
        table.join2(result, package:_generate_lto_configs().ldflags)
    end
    if package:config("asan") then
        table.join2(result, package:_generate_sanitizer_configs("address").ldflags)
    end
    table.join2(result, _get_ldflags_from_packagedeps(package, opt))
    if opt.ldflags then
        table.join2(result, opt.ldflags)
    end
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end

-- get shflags
function _get_shflags(package, opt)
    opt = opt or {}
    local result = {}
    if opt.cross then
        table.join2(result, package:build_getenv("shflags"))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "link", package:build_getenv("links")))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "syslink", package:build_getenv("syslinks")))
        table.join2(result, toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "linkdir", package:build_getenv("linkdirs")))
    end
    table.join2(result, package:config("shflags"))
    if package:config("lto") then
        table.join2(result, package:_generate_lto_configs().shflags)
    end
    if package:config("asan") then
        table.join2(result, package:_generate_sanitizer_configs("address").shflags)
    end
    table.join2(result, _get_ldflags_from_packagedeps(package, opt))
    if opt.shflags then
        table.join2(result, opt.shflags)
    end
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end

-- get arflags
function _get_arflags(package, opt)
    local result = table.wrap(package:build_getenv("arflags"))
    if #result > 0 then
        return os.args(_translate_paths(result))
    end
end
-- get cmake version
function _get_cmake_version()
    local cmake_version = _g.cmake_version
    if not cmake_version then
        local cmake = find_tool("cmake", {version = true})
        if cmake and cmake.version then
            cmake_version = semver.new(cmake.version)
        end
        _g.cmake_version = cmake_version
    end
    return cmake_version
end

function _get_cmake_system_processor(package)
    -- on Windows, CMAKE_SYSTEM_PROCESSOR comes from PROCESSOR_ARCHITECTURE
    -- on other systems it's the output of uname -m
    if package:is_plat("windows") then
        local archs = {
            x86 = "x86",
            x64 = "AMD64",
            x86_64 = "AMD64",
            arm = "ARM",
            arm64 = "ARM64",
            arm64ec = "ARM64EC"
        }
        return archs[package:arch()] or package:arch()
    end
    return package:arch()
end

-- get mingw32 make
function _get_mingw32_make(package)
    local mingw = package:build_getenv("mingw") or package:build_getenv("sdk")
    if mingw then
        local mingw_make = _translate_bin_path(path.join(mingw, "bin", "mingw32-make.exe"))
        if os.isfile(mingw_make) then
            return mingw_make
        end
    end
end

-- get ninja
function _get_ninja(package)
    local ninja = find_tool("ninja")
    if ninja then
        return ninja.program
    end
end

-- https://github.com/xmake-io/xmake-repo/pull/1096
function _fix_cxx_compiler_cmake(package, envs)
    local cxx = envs.CMAKE_CXX_COMPILER
    if cxx and package:has_tool("cxx", "clang", "gcc") then
        local dir = path.directory(cxx)
        local name = path.filename(cxx)
        name = name:gsub("clang$", "clang++")
        name = name:gsub("clang%-", "clang++-")
        name = name:gsub("clang%.", "clang++.")
        name = name:gsub("gcc$", "g++")
        name = name:gsub("gcc%-", "g++-")
        name = name:gsub("gcc%.", "g++.")
        if dir and dir ~= "." then
            cxx = path.join(dir, name)
        else
            cxx = name
        end
        envs.CMAKE_CXX_COMPILER = _translate_bin_path(cxx)
    end
end

-- @see https://github.com/ziglang/zig/issues/22213
function _fix_zigcc_linker_cmake(package, envs)
    if package:has_tool("cc", "zig_cc") then
        envs.CMAKE_C_LINKER_DEPFILE_SUPPORTED = "FALSE"
        envs.CMAKE_CXX_LINKER_DEPFILE_SUPPORTED = "FALSE"
    end
end

-- insert configs from envs
function _insert_configs_from_envs(configs, envs, opt)
    opt = opt or {}
    local configs_str = opt._configs_str
    for k, v in pairs(envs) do
        if configs_str and configs_str:find("-D" .. k .. "=", 1, true) then
            -- use user custom configuration
        else
            table.insert(configs, "-D" .. k .. "=" .. v)
        end
    end
end

-- get configs for generic
function _get_configs_for_generic(package, configs, opt)
    local envs = {}
    if not package:is_plat("windows", "mingw") and package:config("pic") ~= false then
        envs.CMAKE_POSITION_INDEPENDENT_CODE = "ON"
    end
    if not package:use_external_includes() then
        envs.CMAKE_NO_SYSTEM_FROM_IMPORTED = "ON"
    end
    envs.CMAKE_BUILD_TYPE = package:is_debug() and "Debug" or "Release"
    if package:is_library() then
        envs.BUILD_SHARED_LIBS = package:config("shared") and "ON" or "OFF"
    end
    if package:has_source() then
        envs.CMAKE_EXPORT_COMPILE_COMMANDS = "ON"
    end
    -- https://cmake.org/cmake/help/latest/variable/CMAKE_LINKER_TYPE.html
    if package:has_tool("ld", "link") then
        envs.CMAKE_LINKER_TYPE = "MSVC"
    end
    _fix_zigcc_linker_cmake(package, envs)
    _insert_configs_from_envs(configs, envs, opt)
end

-- get configs for windows
function _get_configs_for_windows(package, configs, opt)
    local envs = {}
    local cmake_generator = opt.cmake_generator
    if not cmake_generator or cmake_generator:find("Visual Studio", 1, true) then
        table.insert(configs, "-A")
        if package:is_arch("x86", "i386") then
            table.insert(configs, "Win32")
        elseif package:is_arch("arm64") then
            table.insert(configs, "ARM64")
        elseif package:is_arch("arm64ec") then
            table.insert(configs, "ARM64EC")
        elseif package:is_arch("arm.*") then
            table.insert(configs, "ARM")
        else
            table.insert(configs, "x64")
        end
        local vs_toolset = toolchain_utils.get_vs_toolset_ver(_get_msvc(package):config("vs_toolset") or config.get("vs_toolset"))
        if vs_toolset then
            envs.CMAKE_GENERATOR_TOOLSET = vs_toolset
        end
    end

    -- use clang-cl or clang, and we need pass --target=xxx flags
    if package:has_tool("cc", "clang", "clang_cl") then
        envs.CMAKE_C_COMPILER = _translate_bin_path(package:build_getenv("cc"))
    end
    if package:has_tool("cxx", "clang", "clang_cl") then
        envs.CMAKE_CXX_COMPILER = _translate_bin_path(package:build_getenv("cxx"))
    end

    -- we maybe need patch `cmake_policy(SET CMP0091 NEW)` to enable this argument for some packages
    -- @see https://cmake.org/cmake/help/latest/policy/CMP0091.html#policy:CMP0091
    -- https://github.com/xmake-io/xmake-repo/pull/303
    if package:has_runtime("MT") then
        envs.CMAKE_MSVC_RUNTIME_LIBRARY = "MultiThreaded"
    elseif package:has_runtime("MTd") then
        envs.CMAKE_MSVC_RUNTIME_LIBRARY = "MultiThreadedDebug"
    elseif package:has_runtime("MD") then
        envs.CMAKE_MSVC_RUNTIME_LIBRARY = "MultiThreadedDLL"
    elseif package:has_runtime("MDd") then
        envs.CMAKE_MSVC_RUNTIME_LIBRARY = "MultiThreadedDebugDLL"
    end

    local pdb_dir = path.unix(path.join(os.curdir(), "pdb"))
    envs.CMAKE_COMPILE_PDB_OUTPUT_DIRECTORY = pdb_dir
    envs.CMAKE_PDB_OUTPUT_DIRECTORY = pdb_dir
    _insert_configs_from_envs(configs, envs, opt)

    if package:is_cross() then
        _get_configs_for_cross(package, configs, opt)
    end
end

-- get configs for android
-- https://developer.android.google.cn/ndk/guides/cmake
function _get_configs_for_android(package, configs, opt)
    opt = opt or {}
    local ndk = get_config("ndk")
    if ndk and os.isdir(ndk) then
        local ndk_sdkver = get_config("ndk_sdkver")
        table.insert(configs, "-DCMAKE_TOOLCHAIN_FILE=" .. path.join(ndk, "build/cmake/android.toolchain.cmake"))
        table.insert(configs, "-DANDROID_USE_LEGACY_TOOLCHAIN_FILE=OFF")
        table.insert(configs, "-DANDROID_ABI=" .. package:arch())
        if ndk_sdkver then
            table.insert(configs, "-DANDROID_PLATFORM=android-" .. ndk_sdkver)
            table.insert(configs, "-DANDROID_NATIVE_API_LEVEL=" .. ndk_sdkver)
        end
        -- https://cmake.org/cmake/help/latest/variable/CMAKE_ANDROID_STL_TYPE.html
        local runtime = package:runtimes()
        if runtime then
            table.insert(configs, "-DCMAKE_ANDROID_STL_TYPE=" .. runtime)
        end
        if is_host("windows") and opt.cmake_generator ~= "Ninja" then
            local make = path.join(ndk, "prebuilt", "windows-x86_64", "bin", "make.exe")
            if os.isfile(make) then
                table.insert(configs, "-DCMAKE_MAKE_PROGRAM=" .. make)
            end
        end

        -- avoid find and add system include/library path
        -- @see https://github.com/xmake-io/xmake/issues/2037
        table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH")
        table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH")
        table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH")
        table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER")
    end
end

-- get configs for appleos
function _get_configs_for_appleos(package, configs, opt)
    opt = opt or {}
    local envs                     = {}
    opt.cross                      = true
    -- https://cmake.org/cmake/help/v3.17/manual/cmake-toolchains.7.html#id25
    if package:is_plat("watchos") then
        envs.CMAKE_SYSTEM_NAME = "watchOS"
        if package:is_arch("x86_64", "i386") then
            envs.CMAKE_OSX_SYSROOT = "watchsimulator"
        end
    elseif package:is_plat("iphoneos") then
        envs.CMAKE_SYSTEM_NAME = "iOS"
        if package:is_arch("x86_64", "i386") then
            envs.CMAKE_OSX_SYSROOT = "iphonesimulator"
        end
    elseif package:is_cross() then
        envs.CMAKE_SYSTEM_NAME = "Darwin"
        envs.CMAKE_SYSTEM_PROCESSOR = _get_cmake_system_processor(package)
    end
    envs.CMAKE_OSX_ARCHITECTURES = package:arch()
    envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE   = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY   = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE   = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM   = "NEVER"
    -- avoid install bundle targets
    envs.CMAKE_MACOSX_BUNDLE       = "NO"
    _insert_configs_from_envs(configs, envs, opt)
end

-- get configs for mingw
function _get_configs_for_mingw(package, configs, opt)
    opt = opt or {}
    opt.cross                      = true
    local envs                     = {}
    local sdkdir                   = package:build_getenv("mingw") or package:build_getenv("sdk")
    envs.CMAKE_C_COMPILER          = _translate_bin_path(package:build_getenv("cc"))
    envs.CMAKE_CXX_COMPILER        = _translate_bin_path(package:build_getenv("cxx"))
    envs.CMAKE_ASM_COMPILER        = _translate_bin_path(package:build_getenv("as"))
    envs.CMAKE_AR                  = _translate_bin_path(package:build_getenv("ar"))
    envs.CMAKE_RANLIB              = _translate_bin_path(package:build_getenv("ranlib"))
    envs.CMAKE_RC_COMPILER         = _translate_bin_path(package:build_getenv("mrc"))
    -- @see https://cmake.org/cmake/help/latest/variable/CMAKE_CROSSCOMPILING.html
    -- https://github.com/xmake-io/xmake/pull/5888
    if not is_host("windows") then
        envs.CMAKE_SYSTEM_NAME = "Windows"
    end
    envs.CMAKE_SYSTEM_PROCESSOR    = _get_cmake_system_processor(package)
    -- avoid find and add system include/library path
    -- @see https://github.com/xmake-io/xmake/issues/2037
    -- https://github.com/xmake-io/xmake/issues/6660
    if sdkdir and sdkdir ~= "/usr" then
        envs.CMAKE_FIND_ROOT_PATH = sdkdir
    end
    envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER"
    -- avoid add -isysroot on macOS
    envs.CMAKE_OSX_SYSROOT = ""
    -- Avoid cmake to add the flags -search_paths_first and -headerpad_max_install_names on macOS
    envs.HAVE_FLAG_SEARCH_PATHS_FIRST = "0"
    -- CMAKE_MAKE_PROGRAM may be required for some CMakeLists.txt (libcurl)
    if is_subhost("windows") and opt.cmake_generator ~= "Ninja" then
        envs.CMAKE_MAKE_PROGRAM = _get_mingw32_make(package)
    end
    _fix_cxx_compiler_cmake(package, envs)
    _insert_configs_from_envs(configs, envs, opt)
end

-- get configs for wasm
function _get_configs_for_wasm(package, configs, opt)
    opt = opt or {}
    local envs = {}
    local emsdk = find_emsdk()
    assert(emsdk and emsdk.emscripten, "emscripten not found!")
    local emscripten_cmakefile = find_file("Emscripten.cmake", path.join(emsdk.emscripten, "cmake/Modules/Platform"))
    assert(emscripten_cmakefile, "Emscripten.cmake not found!")
    table.insert(configs, "-DCMAKE_TOOLCHAIN_FILE=" .. emscripten_cmakefile)
    if is_subhost("windows") then
        if opt.cmake_generator ~= "Ninja" then
            local mingw_make = _get_mingw32_make(package)
            if mingw_make then
                table.insert(configs, "-DCMAKE_MAKE_PROGRAM=" .. mingw_make)
            end
        end
    end

    -- avoid find and add system include/library path
    -- @see https://github.com/xmake-io/xmake/issues/5577
    -- https://github.com/emscripten-core/emscripten/issues/13310
    envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER"
    _insert_configs_from_envs(configs, envs, opt)
end

-- get configs for cross
function _get_configs_for_cross(package, configs, opt)
    opt = opt or {}
    opt.cross                      = true
    local envs                     = {}
    local sdkdir                   = _translate_paths(package:build_getenv("sdk"))
    envs.CMAKE_C_COMPILER          = _translate_bin_path(package:build_getenv("cc"))
    envs.CMAKE_CXX_COMPILER        = _translate_bin_path(package:build_getenv("cxx"))
    envs.CMAKE_ASM_COMPILER        = _translate_bin_path(package:build_getenv("as"))
    envs.CMAKE_AR                  = _translate_bin_path(package:build_getenv("ar"))
    if package:is_plat("windows") then
        envs.CMAKE_RC_COMPILER = _translate_bin_path(package:build_getenv("mrc"))
        if package:has_tool("cxx", "cl") then
            envs.CMAKE_AR = path.join(path.directory(envs.CMAKE_CXX_COMPILER), "lib.exe")
        end
    end
    _fix_cxx_compiler_cmake(package, envs)
    -- @note The link command line is set in Modules/CMake{C,CXX,Fortran}Information.cmake and defaults to using the compiler, not CMAKE_LINKER,
    -- so we need to set CMAKE_CXX_LINK_EXECUTABLE to use CMAKE_LINKER as linker.
    --
    -- https://github.com/xmake-io/xmake-repo/pull/1039
    -- https://stackoverflow.com/questions/1867745/cmake-use-a-custom-linker/25274328#25274328
    -- https://github.com/xmake-io/xmake-repo/pull/2134#issuecomment-1573195810
    local ld = _translate_bin_path(package:build_getenv("ld"))
    if package:has_tool("ld", "gxx", "clangxx") then
        envs.CMAKE_CXX_LINK_EXECUTABLE = ld .. " <FLAGS> <CMAKE_CXX_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>"
    end
    envs.CMAKE_RANLIB = _translate_bin_path(package:build_getenv("ranlib"))
    -- we don't need to set it as cross compilation if we just pass toolchain
    -- https://github.com/xmake-io/xmake/issues/2170
    if package:is_cross() then
        local system_name = package:targetos() or "Linux"
        if system_name == "linux" then
            system_name = "Linux"
        elseif system_name == "windows" then
            system_name = "Windows"
        end
        envs.CMAKE_SYSTEM_NAME = system_name
        envs.CMAKE_SYSTEM_PROCESSOR = _get_cmake_system_processor(package)
    end
    -- avoid find and add system include/library path
    -- @see https://github.com/xmake-io/xmake/issues/2037
    -- https://github.com/xmake-io/xmake/issues/6660
    if sdkdir and sdkdir ~= "/usr" then
        envs.CMAKE_FIND_ROOT_PATH = sdkdir
    end
    envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH"
    envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER"
    -- avoid add -isysroot on macOS
    envs.CMAKE_OSX_SYSROOT = ""
    -- avoid cmake to add the flags -search_paths_first and -headerpad_max_install_names on macOS
    envs.HAVE_FLAG_SEARCH_PATHS_FIRST = "0"
    -- avoids finding host include/library path
    envs.CMAKE_FIND_USE_CMAKE_SYSTEM_PATH = "0"
    envs.CMAKE_FIND_USE_INSTALL_PREFIX = "0"
    _insert_configs_from_envs(configs, envs, opt)
end

-- get configs for host toolchain
function _get_configs_for_host_toolchain(package, configs, opt)
    opt = opt or {}
    opt.cross                      = true
    local envs                     = {}
    envs.CMAKE_C_COMPILER          = _translate_bin_path(package:build_getenv("cc"))
    envs.CMAKE_CXX_COMPILER        = _translate_bin_path(package:build_getenv("cxx"))
    envs.CMAKE_ASM_COMPILER        = _translate_bin_path(package:build_getenv("as"))
    envs.CMAKE_RC_COMPILER         = _translate_bin_path(package:build_getenv("mrc"))
    envs.CMAKE_AR                  = _translate_bin_path(package:build_getenv("ar"))
    _fix_cxx_compiler_cmake(package, envs)
    -- @note The link command line is set in Modules/CMake{C,CXX,Fortran}Information.cmake and defaults to using the compiler, not CMAKE_LINKER,
    -- so we need set CMAKE_CXX_LINK_EXECUTABLE to use CMAKE_LINKER as linker.
    --
    -- https://github.com/xmake-io/xmake-repo/pull/1039
    -- https://stackoverflow.com/questions/1867745/cmake-use-a-custom-linker/25274328#25274328
    -- https://github.com/xmake-io/xmake-repo/pull/2134#issuecomment-1573195810
    local ld = _translate_bin_path(package:build_getenv("ld"))
    if package:has_tool("ld", "gxx", "clangxx") then
        envs.CMAKE_CXX_LINK_EXECUTABLE = ld .. " <FLAGS> <CMAKE_CXX_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>"
    end
    envs.CMAKE_RANLIB = _translate_bin_path(package:build_getenv("ranlib"))
    -- we don't need to set it as cross compilation if we just pass toolchain
    -- https://github.com/xmake-io/xmake/issues/2170
    if package:is_cross() then
        envs.CMAKE_SYSTEM_NAME     = "Linux"
    end
    _insert_configs_from_envs(configs, envs, opt)
end

-- get cmake generator for msvc
function _get_cmake_generator_for_msvc(package)
    local vs = _get_msvc(package):config("vs") or config.get("vs")
    return "Visual Studio " .. toolchain_utils.get_vsver(vs) .. " " .. vs
end

-- get configs for cmake generator
function _get_configs_for_generator(package, configs, opt)
    opt     = opt or {}
    configs = configs or {}
    local cmake_generator = opt.cmake_generator
    if cmake_generator then
        if cmake_generator:find("Visual Studio", 1, true) then
            cmake_generator = _get_cmake_generator_for_msvc(package)
        end
        table.insert(configs, "-G")
        table.insert(configs, cmake_generator)
        if cmake_generator:find("Ninja", 1, true) then
            local jobs = _get_parallel_njobs(opt)
            local linkjobs = opt.linkjobs or option.get("linkjobs")
            if linkjobs then
                table.insert(configs, "-DCMAKE_JOB_POOL_COMPILE:STRING=compile")
                table.insert(configs, "-DCMAKE_JOB_POOL_LINK:STRING=link")
                table.insert(configs, ("-DCMAKE_JOB_POOLS:STRING=compile=%s;link=%s"):format(jobs, linkjobs))
            end
            local ninja = _get_ninja(package)
            if ninja then
                table.insert(configs, "-DCMAKE_MAKE_PROGRAM=" .. ninja)
            end
        end
    elseif package:is_plat("mingw") and is_subhost("msys") then
        table.insert(configs, "-G")
        table.insert(configs, "MSYS Makefiles")
    elseif package:is_plat("mingw") and is_subhost("windows") then
        table.insert(configs, "-G")
        table.insert(configs, "MinGW Makefiles")
    elseif package:is_plat("windows") then
        table.insert(configs, "-G")
        table.insert(configs, _get_cmake_generator_for_msvc(package))
    elseif package:is_plat("wasm") and is_subhost("windows") then
        table.insert(configs, "-G")
        table.insert(configs, "MinGW Makefiles")
    else
        table.insert(configs, "-G")
        table.insert(configs, "Unix Makefiles")
    end
end

-- get configs for installation
function _get_configs_for_install(package, configs, opt)
    -- @see https://cmake.org/cmake/help/v3.14/module/GNUInstallDirs.html
    -- LIBDIR: object code libraries (lib or lib64 or lib/<multiarch-tuple> on Debian)
    --
    table.insert(configs, "-DCMAKE_INSTALL_PREFIX=" .. package:installdir())
    if not opt._configs_str:find("CMAKE_INSTALL_LIBDIR") then
        table.insert(configs, "-DCMAKE_INSTALL_LIBDIR:PATH=lib")
    end
end

function _get_default_flags(package, configs, buildtype, opt)
    -- The default flags are different for different platforms
    -- @see https://github.com/xmake-io/xmake-repo/pull/4038#issuecomment-2116489448
    local cc = package:build_getenv("cc")
    local cxx = package:build_getenv("cxx")
    local cachekey = buildtype .. package:plat() .. package:arch() .. (cc or "") .. (cxx or "")
    local cmake_default_flags = _g.cmake_default_flags and _g.cmake_default_flags[cachekey]
    if not cmake_default_flags then
        local tmpdir = path.join(os.tmpfile() .. ".dir", package:displayname(), package:mode())
        local dummy_cmakelist = path.join(tmpdir, "CMakeLists.txt")

        local cflags
        if cc then
            cflags = "-DCMAKE_C_COMPILER=" .. _translate_bin_path(cc)
        end
        local cxxflags
        if cxx then
            cxxflags = "-DCMAKE_CXX_COMPILER=" .. _translate_bin_path(cxx)
        end

        -- About the minimum cmake version requirement
        -- @see https://github.com/xmake-io/xmake/pull/6032
        io.writefile(dummy_cmakelist, format([[
    cmake_minimum_required(VERSION 3.15)
    project(XMakeDummyProject)

    message(STATUS "CMAKE_C_FLAGS is ${CMAKE_C_FLAGS}")
    message(STATUS "CMAKE_C_FLAGS_%s is ${CMAKE_C_FLAGS_%s}")

    message(STATUS "CMAKE_CXX_FLAGS is ${CMAKE_CXX_FLAGS}")
    message(STATUS "CMAKE_CXX_FLAGS_%s is ${CMAKE_CXX_FLAGS_%s}")

    message(STATUS "CMAKE_EXE_LINKER_FLAGS is ${CMAKE_EXE_LINKER_FLAGS}")
    message(STATUS "CMAKE_EXE_LINKER_FLAGS_%s is ${CMAKE_EXE_LINKER_FLAGS_%s}")

    message(STATUS "CMAKE_SHARED_LINKER_FLAGS is ${CMAKE_SHARED_LINKER_FLAGS}")
    message(STATUS "CMAKE_SHARED_LINKER_FLAGS_%s is ${CMAKE_SHARED_LINKER_FLAGS_%s}")

    message(STATUS "CMAKE_STATIC_LINKER_FLAGS is ${CMAKE_STATIC_LINKER_FLAGS}")
    message(STATUS "CMAKE_STATIC_LINKER_FLAGS_%s is ${CMAKE_STATIC_LINKER_FLAGS_%s}")
        ]], buildtype, buildtype, buildtype, buildtype, buildtype, buildtype, buildtype, buildtype, buildtype, buildtype))

        local runenvs = opt.envs or buildenvs(package)
        local cmake = find_tool("cmake")
        local _configs = table.join(configs, "-S", path.directory(dummy_cmakelist), "-B", tmpdir, cflags or {}, cxxflags)
        local outdata = try{ function() return os.iorunv(cmake.program, _configs, {envs = runenvs}) end}
        if outdata and outdata ~= "" then
            cmake_default_flags = {}
            cmake_default_flags.CMAKE_C_FLAGS = outdata:match("CMAKE_C_FLAGS is (.-)\n")
            cmake_default_flags["CMAKE_C_FLAGS_" .. buildtype] = outdata:match(format("CMAKE_C_FLAGS_%s is (.-)\n", buildtype))
            cmake_default_flags.CMAKE_CXX_FLAGS = outdata:match("CMAKE_CXX_FLAGS is (.-)\n")
            cmake_default_flags["CMAKE_CXX_FLAGS_" .. buildtype] = outdata:match(format("CMAKE_CXX_FLAGS_%s is (.-)\n", buildtype))
            cmake_default_flags.CMAKE_CXX_FLAGS = outdata:match("CMAKE_CXX_FLAGS is (.-)\n")
            cmake_default_flags["CMAKE_EXE_LINKER_FLAGS_" .. buildtype] = outdata:match(format("CMAKE_EXE_LINKER_FLAGS_%s is (.-)\n", buildtype))
            cmake_default_flags.CMAKE_SHARED_LINKER_FLAGS = outdata:match("CMAKE_SHARED_LINKER_FLAGS is (.-)\n")
            cmake_default_flags["CMAKE_SHARED_LINKER_FLAGS_" .. buildtype] = outdata:match(format("CMAKE_SHARED_LINKER_FLAGS_%s is (.-)\n", buildtype))
            cmake_default_flags.CMAKE_STATIC_LINKER_FLAGS = outdata:match("CMAKE_STATIC_LINKER_FLAGS is (.-)\n")
            cmake_default_flags["CMAKE_STATIC_LINKER_FLAGS_" .. buildtype] = outdata:match(format("CMAKE_STATIC_LINKER_FLAGS_%s is (.-)\n", buildtype))
            for k, v in ipairs(cmake_default_flags) do
                cmake_default_flags[k] = v:replace("/M[DT]d", ""):replace("/M[DT]", "")
            end
            _g.cmake_default_flags = _g.cmake_default_flags or {}
            _g.cmake_default_flags[cachekey] = cmake_default_flags
        end
        os.rm(tmpdir)
    end
    return cmake_default_flags
end

function _get_cmake_buildtype(package)
    local cmake_buildtype_map = {
        debug = "DEBUG",
        release = "RELEASE",
        releasedbg = "RELWITHDEBINFO"
    }
    local buildtype = package:mode()
    return cmake_buildtype_map[buildtype] or "RELEASE"
end

function _get_envs_for_default_flags(package, configs, opt)
    local buildtype = _get_cmake_buildtype(package)
    return table.clone(_get_default_flags(package, configs, buildtype, opt)) or {}
end

function _get_envs_for_runtime_flags(package, opt)
    local buildtype = _get_cmake_buildtype(package)
    local envs = {}
    local runtimes = package:runtimes()
    if runtimes then
        envs[format("CMAKE_C_FLAGS_%s", buildtype)]             = toolchain_utils.map_compflags_for_package(package, "c", "runtime", runtimes)
        envs[format("CMAKE_CXX_FLAGS_%s", buildtype)]           = toolchain_utils.map_compflags_for_package(package, "cxx", "runtime", runtimes)
        envs[format("CMAKE_EXE_LINKER_FLAGS_%s", buildtype)]    = toolchain_utils.map_linkflags_for_package(package, "binary", {"cxx"}, "runtime", runtimes)
        envs[format("CMAKE_STATIC_LINKER_FLAGS_%s", buildtype)] = toolchain_utils.map_linkflags_for_package(package, "static", {"cxx"}, "runtime", runtimes)
        envs[format("CMAKE_SHARED_LINKER_FLAGS_%s", buildtype)] = toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "runtime", runtimes)
        envs[format("CMAKE_MODULE_LINKER_FLAGS_%s", buildtype)] = toolchain_utils.map_linkflags_for_package(package, "shared", {"cxx"}, "runtime", runtimes)
    end
    return envs
end

function _get_envs_for_flags(package, configs, opt)
    -- get the default envs
    local envs = _get_envs_for_default_flags(package, configs, opt)
    local runtime_envs = _get_envs_for_runtime_flags(package, opt)
    if runtime_envs then
        for name, value in pairs(runtime_envs) do
            envs[name] = (envs[name] or " ") .. " " .. table.concat(value, " ")
        end
    end

    -- get the platform/toolchain envs
    local platform_envs = {}
    if package:is_plat("windows") then
        -- use clang-cl or clang, and we need pass --target=xxx flags
        if package:has_tool("cc", "clang", "clang_cl") then
            -- @see https://github.com/xmake-io/xmake-repo/issues/7662
            platform_envs.CMAKE_C_FLAGS = _get_cflags(package, table.join({cross = true}, opt))
        end
        if package:has_tool("cxx", "clang", "clang_cl") then
            platform_envs.CMAKE_CXX_FLAGS = _get_cxxflags(package, table.join({cross = true}, opt))
        end
    elseif package:is_plat("wasm") then
        -- pass toolchain flags cross-compilation
        -- @see https://github.com/xmake-io/xmake/issues/6690
        opt.cross = true
    end
    platform_envs.CMAKE_C_FLAGS             = platform_envs.CMAKE_C_FLAGS or _get_cflags(package, opt)
    platform_envs.CMAKE_CXX_FLAGS           = platform_envs.CMAKE_CXX_FLAGS or _get_cxxflags(package, opt)
    platform_envs.CMAKE_ASM_FLAGS           = _get_asflags(package, opt)
    platform_envs.CMAKE_EXE_LINKER_FLAGS    = _get_ldflags(package, opt)
    platform_envs.CMAKE_SHARED_LINKER_FLAGS = _get_shflags(package, opt)
    platform_envs.CMAKE_MODULE_LINKER_FLAGS = _get_shflags(package, opt)
    platform_envs.CMAKE_STATIC_LINKER_FLAGS = _get_arflags(package, opt)
    for name, value in pairs(platform_envs) do
        envs[name] = (envs[name] or " ") .. " " .. value
    end
    return envs
end

function _get_configs(package, configs, opt)
    configs = configs or {}
    opt._configs_str = string.serialize(configs, {indent = false, strip = true})
    _get_configs_for_install(package, configs, opt)
    _get_configs_for_generator(package, configs, opt)
    _get_configs_for_generic(package, configs, opt)

    if package:is_plat("windows") then
        _get_configs_for_windows(package, configs, opt)
    elseif package:is_plat("android") then
        _get_configs_for_android(package, configs, opt)
    elseif package:is_plat("iphoneos", "watchos") or
        -- for cross-compilation on macOS, @see https://github.com/xmake-io/xmake/issues/2804
        (package:is_plat("macosx") and (get_config("appledev") or not package:is_arch(os.subarch()))) then
        _get_configs_for_appleos(package, configs, opt)
    elseif package:is_plat("mingw") then
        _get_configs_for_mingw(package, configs, opt)
    elseif package:is_plat("wasm") then
        _get_configs_for_wasm(package, configs, opt)
    elseif package:is_cross() then
        _get_configs_for_cross(package, configs, opt)
    elseif package:config("toolchains") then
        -- we still need find system libraries,
        -- it just pass toolchain environments if the toolchain is compatible with host
        if _is_toolchain_compatible_with_host(package) then
            _get_configs_for_host_toolchain(package, configs, opt)
        else
            _get_configs_for_cross(package, configs, opt)
        end
    end

    -- fix error for cmake 4.x
    -- e.g. Compatibility with CMake < 3.5 has been removed from CMake.
    if _get_cmake_version() and _get_cmake_version():ge("4.0") then
        table.insert(configs, "-DCMAKE_POLICY_VERSION_MINIMUM=3.5")
    end

    -- insert flags to configs
    --
    -- @note We need to rely on configs to get the exact default flags
    -- @see https://github.com/xmake-io/xmake/issues/6781
    local envs = _get_envs_for_flags(package, configs, opt)
    _insert_configs_from_envs(configs, envs, opt)

    -- enable ccache?
    local ccache = package:data("ccache")
    if ccache then
        table.insert(configs, "-DCMAKE_C_COMPILER_LAUNCHER=" .. ccache)
        table.insert(configs, "-DCMAKE_CXX_COMPILER_LAUNCHER=" .. ccache)
    end
    return configs
end

-- Fix pdb issue, if multiple CL.EXE write to the same .PDB file, please use /FS
-- @see https://github.com/xmake-io/xmake/issues/5353
function _fix_pdbdir_for_ninja(package)
    if package:is_plat("windows") and package:has_tool("cxx", "cl") then
        local pdbdir = "pdb"
        if not os.isdir(pdbdir) then
            os.mkdir(pdbdir)
        end
    end
end

-- enter build directory
function _enter_builddir(package, opt)
    local builddir = opt.builddir or opt.buildir or package:builddir()
    if opt.buildir then
        wprint("{buildir = } has been deprecated, please use {builddir = } in cmake.install")
    end
    os.mkdir(path.join(builddir, "install"))
    return os.cd(builddir)
end

-- get build environments
function buildenvs(package, opt)

    -- we need to bind msvc environments manually
    -- @see https://github.com/xmake-io/xmake/issues/1057
    opt = opt or {}
    local envs = {}
    if package:is_plat("windows") then
        envs = _get_msvc_runenvs(package)
    end

    -- we need to pass pkgconf for windows/mingw without msys2/cygwin
    if package:is_plat("windows", "mingw") and is_subhost("windows") then
        local pkgconf = _get_pkgconfig(package)
        if pkgconf then
            envs.PKG_CONFIG = pkgconf
        end
    end

    -- add environments for cmake/find_packages
    -- and we need also find them from private libraries,
    -- @see https://github.com/xmake-io/xmake-repo/pull/2553
    local CMAKE_LIBRARY_PATH = {}
    local CMAKE_INCLUDE_PATH = {}
    local CMAKE_PREFIX_PATH  = {}
    local PKG_CONFIG_PATH = {}
    for _, dep in ipairs(package:librarydeps({private = true})) do
        if dep:is_system() then
            local fetchinfo = dep:fetch()
            if fetchinfo then
                table.join2(CMAKE_LIBRARY_PATH, fetchinfo.linkdirs)
                table.join2(CMAKE_INCLUDE_PATH, fetchinfo.includedirs)
                table.join2(CMAKE_INCLUDE_PATH, fetchinfo.sysincludedirs)
            end
        else
            table.join2(CMAKE_PREFIX_PATH, dep:installdir())
            local pkgconfig = path.join(dep:installdir(), "lib", "pkgconfig")
            if os.isdir(pkgconfig) then
                table.insert(PKG_CONFIG_PATH, pkgconfig)
            end
            pkgconfig = path.join(dep:installdir(), "share", "pkgconfig")
            if os.isdir(pkgconfig) then
                table.insert(PKG_CONFIG_PATH, pkgconfig)
            end
        end
    end
    envs.CMAKE_LIBRARY_PATH = path.joinenv(CMAKE_LIBRARY_PATH)
    envs.CMAKE_INCLUDE_PATH = path.joinenv(CMAKE_INCLUDE_PATH)
    envs.CMAKE_PREFIX_PATH  = path.joinenv(CMAKE_PREFIX_PATH)
    envs.PKG_CONFIG_PATH    = path.joinenv(PKG_CONFIG_PATH)
    return envs
end

-- do build for msvc
function _build_for_msvc(package, configs, opt)
    local allbuild = os.isfile("ALL_BUILD.vcxproj") and "ALL_BUILD.vcxproj" or "ALL_BUILD.vcproj"
    assert(os.isfile(allbuild), "ALL_BUILD project not found!")
    msbuild.build(package, {allbuild, "-t:Rebuild"}, opt)
end

-- do build for make
function _build_for_make(package, configs, opt)
    local argv = {}
    local targets = table.wrap(opt.targets or opt.target)
    if #targets ~= 0 then
        table.join2(argv, targets)
    end
    local jobs = _get_parallel_njobs(opt)
    table.insert(argv, "-j" .. jobs)
    if option.get("diagnosis") then
        table.insert(argv, "VERBOSE=1")
    end
    if is_host("bsd") then
        os.vrunv("gmake", argv)
    elseif is_subhost("windows") and package:is_plat("mingw") then
        local mingw_make = assert(_get_mingw32_make(package), "mingw32-make.exe not found!")
        os.vrunv(mingw_make, argv)
    elseif package:is_plat("android") and is_host("windows") then
        local make
        local ndk = get_config("ndk")
        if ndk then
            make = path.join(ndk, "prebuilt", "windows-x86_64", "bin", "make.exe")
        end
        if not make or not os.isfile(make) then
            make = "make"
        end
        os.vrunv(make, argv)
    else
        os.vrunv("make", argv)
    end
end

-- do build for ninja
function _build_for_ninja(package, configs, opt)
    opt = opt or {}
    _fix_pdbdir_for_ninja(package)
    ninja.build(package, {}, {envs = opt.envs or buildenvs(package, opt),
        jobs = opt.jobs,
        targets = opt.targets or opt.target})
end

-- do build for cmake/build
function _build_for_cmakebuild(package, configs, opt)
    local cmake = assert(find_tool("cmake"), "cmake not found!")
    local argv = {"--build", os.curdir()}
    if opt.config then
        table.insert(argv, "--config")
        table.insert(argv, opt.config)
    end
    local targets = table.wrap(opt.targets or opt.target)
    if #targets ~= 0 then
        table.insert(argv, "--target")
        if #targets > 1 then
            -- https://stackoverflow.com/questions/47553569/how-can-i-build-multiple-targets-using-cmake-build
            if _get_cmake_version() and _get_cmake_version():ge("3.15") then
                table.join2(argv, targets)
            else
                raise("Build multiple targets need cmake >=3.15")
            end
        else
            table.insert(argv, targets[1])
        end
    end
    os.vrunv(cmake.program, argv, {envs = opt.envs or buildenvs(package)})
end

-- do install for msvc
function _install_for_msvc(package, configs, opt)
    local allbuild = os.isfile("ALL_BUILD.vcxproj") and "ALL_BUILD.vcxproj" or "ALL_BUILD.vcproj"
    assert(os.isfile(allbuild), "ALL_BUILD project not found!")
    msbuild.build(package, {allbuild, "-t:Rebuild", "/nr:false"}, opt)
    local projfile = os.isfile("INSTALL.vcxproj") and "INSTALL.vcxproj" or "INSTALL.vcproj"
    if os.isfile(projfile) then
        msbuild.build(package, {projfile}, opt)
        os.trycp("install/bin", package:installdir())
        os.trycp("install/lib", package:installdir()) -- perhaps only headers library
        os.trycp("install/share", package:installdir())
        os.trycp("install/include", package:installdir())
    else
        os.trycp("**.dll", package:installdir("bin"))
        os.trycp("**.lib", package:installdir("lib"))
        os.trycp("**.exp", package:installdir("lib"))
        if package:config("shared") or not package:is_library() then
            os.trycp("**.pdb", package:installdir("bin"))
        else
            os.trycp("**.pdb", package:installdir("lib"))
        end
    end
end

-- do install for make
function _install_for_make(package, configs, opt)
    local jobs = _get_parallel_njobs(opt)
    local argv = {"-j" .. jobs}
    if option.get("diagnosis") then
        table.insert(argv, "VERBOSE=1")
    end
    if is_host("bsd") then
        os.vrunv("gmake", argv)
        os.vrunv("gmake", {"install"})
    elseif is_subhost("windows") and package:is_plat("mingw", "wasm") then
        local mingw_make = assert(_get_mingw32_make(package), "mingw32-make.exe not found!")
        os.vrunv(mingw_make, argv)
        os.vrunv(mingw_make, {"install"})
    elseif package:is_plat("android") and is_host("windows") then
        local make
        local ndk = get_config("ndk")
        if ndk then
            make = path.join(ndk, "prebuilt", "windows-x86_64", "bin", "make.exe")
        end
        if not make or not os.isfile(make) then
            make = "make"
        end
        os.vrunv(make, argv)
        os.vrunv(make, {"install"})
    else
        os.vrunv("make", argv)
        os.vrunv("make", {"install"})
    end
end

-- do install for ninja
function _install_for_ninja(package, configs, opt)
    opt = opt or {}
    _fix_pdbdir_for_ninja(package)
    ninja.install(package, {}, {envs = opt.envs or buildenvs(package, opt),
        jobs = opt.jobs,
        targets = opt.targets or opt.target})
end

-- do install for cmake/build
function _install_for_cmakebuild(package, configs, opt)
    opt = opt or {}
    local cmake = assert(find_tool("cmake"), "cmake not found!")
    local argv = {"--build", os.curdir()}
    local install_argv = {"--install", os.curdir()}
    if opt.config then
        table.insert(argv, "--config")
        table.insert(argv, opt.config)
        table.insert(install_argv, "--config")
        table.insert(install_argv, opt.config)
    end
    os.vrunv(cmake.program, argv, {envs = opt.envs or buildenvs(package)})
    os.vrunv(cmake.program, install_argv)
end

-- get cmake generator
function _get_cmake_generator(package, opt)
    opt = opt or {}
    local cmake_generator = opt.cmake_generator
    if not cmake_generator then
        local use_ninja = package:policy("package.cmake_generator.ninja")
        if use_ninja == nil then
            use_ninja = project.policy("package.cmake_generator.ninja")
        end
        if use_ninja then
            cmake_generator = "Ninja"
        end
        if not cmake_generator then
            if package:has_tool("cc", "clang_cl") or package:has_tool("cxx", "clang_cl") then
                cmake_generator = "Ninja"
            elseif (is_subhost("windows") and package:is_plat("mingw", "wasm"))
                or (package:is_plat("windows") and is_host("linux")) then
                local ninja = _get_ninja(package)
                if ninja then
                    cmake_generator = "Ninja"
                end
            end
        end
        local cmake_generator_env = os.getenv("CMAKE_GENERATOR")
        if not cmake_generator and cmake_generator_env then
            cmake_generator = cmake_generator_env
        end
        if cmake_generator then
            opt.cmake_generator = cmake_generator
        end
    end
    return cmake_generator
end

-- shrink cmake arguments, fix too long arguments
-- @see https://github.com/xmake-io/xmake-repo/pull/5247#discussion_r1780302212
function _shrink_cmake_arguments(argv, oldir, opt)
    local cmake_argv = {}
    local long_options = hashset.of(
        "CMAKE_C_FLAGS",
        "CMAKE_CXX_FLAGS",
        "CMAKE_ASM_FLAGS",
        "CMAKE_EXE_LINKER_FLAGS",
        "CMAKE_SHARED_LINKER_FLAGS",
        "CMAKE_MODULE_LINKER_FLAGS",
        "CMAKE_C_FLAGS_RELEASE",
        "CMAKE_CXX_FLAGS_RELEASE",
        "CMAKE_ASM_FLAGS_RELEASE",
        "CMAKE_EXE_LINKER_FLAGS_RELEASE",
        "CMAKE_SHARED_LINKER_FLAGS_RELEASE",
        "CMAKE_MODULE_LINKER_FLAGS_RELEASE",
        "CMAKE_C_FLAGS_DEBUG",
        "CMAKE_CXX_FLAGS_DEBUG",
        "CMAKE_ASM_FLAGS_DEBUG",
        "CMAKE_EXE_LINKER_FLAGS_DEBUG",
        "CMAKE_SHARED_LINKER_FLAGS_DEBUG",
        "CMAKE_MODULE_LINKER_FLAGS_DEBUG")
    local shrink = false
    local add_compile_options = false
    local add_link_options = false
    if _get_cmake_version() and _get_cmake_version():ge("3.13") then
        add_compile_options = true
        add_link_options = true
    end
    local buildtypes_map = {
        RELEASE = "Release",
        DEBUG = "Debug",
        RELWITHDEBINFO = "RelWithDebInfo"
    }
    table.remove_if(argv, function (idx, value)
        local k, v = value:match("%-D(.*)=(.*)")
        if k and v and long_options:has(k) then
            local kind, mode = k:match("CMAKE_(.+)_FLAGS_(.+)")
            if not kind then
                kind = k:match("CMAKE_(.+)_FLAGS")
            end
            -- improve cmake flags
            -- @see https://github.com/xmake-io/xmake/issues/5826
            --[[
            local build_type = mode and buildtypes_map[mode] or nil
            if #v > 0 and add_compile_options and (kind == "C" or kind == "CXX" or kind == "ASM") then
                if build_type then
                    table.insert(cmake_argv, ("if(CMAKE_BUILD_TYPE STREQUAL \"%s\")"):format(build_type))
                end
                local flags = v:replace("\"", "\\\"")
                table.insert(cmake_argv, ("set(COMP_%s_FLAGS \"%s\")"):format(kind, flags))
                table.insert(cmake_argv, ("add_compile_options($<$<COMPILE_LANGUAGE:%s>:${COMP_%s_FLAGS}>)"):format(kind, kind))
                if build_type then
                    table.insert(cmake_argv, "endif()")
                end
                shrink = true
                return true
            end]]
            -- shrink long arguments
            if #v > 128 then
                local flags = v:replace("\"", "\\\"")
                table.insert(cmake_argv, ("set(%s \"%s\")"):format(k, flags))
                shrink = true
                return true
            end
        end
    end)
    if shrink then
        local cmakefile = path.join(opt.curdir and opt.curdir or oldir, "CMakeLists.txt")
        io.insert(cmakefile, 1, table.concat(cmake_argv, "\n"))
    end
end

function configure(package, configs, opt)
    opt = opt or {}
    local oldir = _enter_builddir(package, opt)

    -- pass configurations
    local argv = {}
    for name, value in pairs(_get_configs(package, configs, opt)) do
        value = tostring(value):trim()
        if type(name) == "number" then
            if value ~= "" then
                table.insert(argv, value)
            end
        else
            table.insert(argv, "-D" .. name .. "=" .. value)
        end
    end
    -- shrink cmake arguments, fix too long arguments
    -- @see https://github.com/xmake-io/xmake-repo/pull/5247#discussion_r1780302212
    _shrink_cmake_arguments(argv, oldir, opt)
    table.insert(argv, oldir)

    -- do configure
    local cmake = assert(find_tool("cmake"), "cmake not found!")
    os.vrunv(cmake.program, argv, {envs = opt.envs or buildenvs(package, opt)})
    os.cd(oldir)
end

-- build package
function build(package, configs, opt)
    opt = opt or {}
    local cmake_generator = _get_cmake_generator(package, opt)

    -- do configure
    configure(package, configs, opt)

    -- do build
    local oldir = _enter_builddir(package, opt)
    if opt.cmake_build then
        _build_for_cmakebuild(package, configs, opt)
    elseif cmake_generator then
        if cmake_generator:find("Visual Studio", 1, true) then
            _build_for_msvc(package, configs, opt)
        elseif cmake_generator == "Ninja" then
            _build_for_ninja(package, configs, opt)
        elseif cmake_generator:find("Makefiles", 1, true) then
            _build_for_make(package, configs, opt)
        else
            raise("unknown cmake generator(%s)!", cmake_generator)
        end
    else
        if package:is_plat("windows") then
            _build_for_msvc(package, configs, opt)
        else
            _build_for_make(package, configs, opt)
        end
    end
    os.cd(oldir)
end

-- install package
function install(package, configs, opt)
    opt = opt or {}
    local cmake_generator = _get_cmake_generator(package, opt)

    -- do configure
    configure(package, configs, opt)

    -- do build and install
    local oldir = _enter_builddir(package, opt)
    if opt.cmake_build then
        _install_for_cmakebuild(package, configs, opt)
    elseif cmake_generator then
        if cmake_generator:find("Visual Studio", 1, true) then
            _install_for_msvc(package, configs, opt)
        elseif cmake_generator == "Ninja" then
            _install_for_ninja(package, configs, opt)
        elseif cmake_generator:find("Makefiles", 1, true) then
            _install_for_make(package, configs, opt)
        else
            raise("unknown cmake generator(%s)!", cmake_generator)
        end
    else
        if package:is_plat("windows") then
            _install_for_msvc(package, configs, opt)
        else
            _install_for_make(package, configs, opt)
        end
    end
    if package:is_plat("windows") and os.isdir("pdb") then
        if package:config("shared") or not package:is_library() then
            os.trycp("pdb/**.pdb", package:installdir("bin"))
        else
            os.trycp("pdb/**.pdb", package:installdir("lib"))
        end
    end
    os.cd(oldir)
end
