build.lua#

The following file is the essential build.lua premake file included with repo_build.

Put simply, this file is analogous to the “main function” of all projects for which repo_build is used to build, and are compiled using a c++ toolchain.

It’s intended to make it easier to build with Omniverse-centric defaults, and to help ensure that projects are built correctly regardless of the platform or toolchain used. It also establishes certain conventions, such as how packman dependency files are often split between host and target dependencies, and that those dependencies would then be linked into different subfolders of the _build directory, or how to find the chosen compiler in the current project.

It’s included in these docs as a reference, because its options and arguments represent an important interface that may be useful to refer to in the same way that a CLI or API might be referred to.

local m = {} -- The main module table

-- This is the root of the repo.
-- This just uses the %{root} token, but in most cases you will want to explicitly set this to the
-- root of your repo.
m.root = "%{root}"

require('vstudio')
if no_compile_commands_file ~= true then
    require('build/compilecommands')
end

-- Configure the standard command line options for an Omniverse project.
-- This is intended to be run in the global scope for a project.
-- @returns nil
function m.setup_options()
    -- This seems to exist because repo_build passes --platform-host, rather than it actually being
    -- used by anything anymore.
    newoption {
        trigger     = "platform-host",
        description = "Specify host platform the build is running on."
    }

    -- This is also not used by anything. repo_build will pass this if
    -- repo_build.premake.pass_platform_target is set to true in repo.toml
    newoption {
        trigger     = "platform-target",
        description = "(Optional) Specify target platform we're intending to build."
    }

    newoption {
        trigger     = "no-fatal-warnings",
        description = "(Optional) disable the fatal warnings flag."
            .." This will prevent warnings from causing the build to fail."
            .." This can be useful when writing test code or testing a new toolchain."
    }

    newoption {
        trigger     = "enable-ubsan",
        description = "(Optional) enable the undefined behavior sanitizer, if supported. "
            .." Build artifacts from a non-ubsan build must be cleaned since every translation unit"
            .." must be recompiled with ubsan."
            .." This causes undefined behavior to be detected at runtime."
            .." This causes a ~20% performance hit to the compiled program when enabled."
            .." Ubsan can be mixed with asan."
            .." If you want to see stack traces from ubsan errors, set the environment variable"
            .." UBSAN_OPTIONS=print_stacktrace=1."
            .." Ubsan warns about unsigned overflow even though it is not undefined behavior; if "
            .." you want to ignore this, set the environment variable "
            .." UBSAN_OPTIONS=silence_unsigned_overflow=1."
    }

    newoption {
        trigger     = "enable-asan",
        description = "(Optional) enable the address sanitizer (memory error detection), if supported."
            .." This is similar to valgrind's memory error detection, except with a greatly improved"
            .." performance and the ability to detect stack memory errors."
            .." Build artifacts from a non-asan build must be cleaned since every translation unit"
            .." must be recompiled with asan."
            .." Asan can be mixed with ubsan."
            .." This causes a ~50% performance hit to the compiled program when enabled."
            .." This also increases the physical memory usage by over 200%."
            .." Virtual memory usage from asan is in the terabyte range, so this could conflict "
            .." with some sparse memory techniques."
            .." This option disabled warnings-as-errors due to some false positives it creates."
            .." Setting the environment variable ASAN_OPTIONS=halt_on_error=0 will prevent the "
            .." application from exiting when a memory error is encountered."
            .." Setting the environment variable ASAN_OPTIONS=detect_leaks=0 will disable leak detection."
            .." If you notice that the printed stack traces on memory leaks are incomplete, try"
            .." setting the environment variable ASAN_OPTIONS=fast_unwind_on_malloc=0."
            .." Note that this will copy libasan.so to the _build directory (because of GCC bug "
            .." 64234), so this should not be used for release builds. No copy is performed when "
            .." combining this with --no-docker, so you may encounter runtime linking errors."
    }

    newoption {
        trigger     = "enable-lsan",
        description = "(Optional) enable the memory leak sanitizer, if supported."
            .." This offers the memory leak tracking of lsan but without the additional costs "
            .." incurred by asan."
            .." Build artifacts from a non-lsan build must be cleaned since every translation unit"
            .." must be recompiled with lsan."
            .." If you notice that the printed stack traces on memory leaks are incomplete, try"
            .." setting the environment variable LSAN_OPTIONS=fast_unwind_on_malloc=0."
    }

    newoption {
        trigger     = "enable-tsan",
        description = "(Optional) enable the thread sanitizer, if supported."
            .." This is similar to helgrind, except faster. This can reduce processing speed by "
            .." over 10x and increase physical memory usage by over 3x."
            .." Build artifacts from a non-tsan build must be cleaned since every translation unit"
            .." must be recompiled with tsan."
            .." Note that this will copy libtsan.so to the _build directory (because of GCC bug "
            .." 64234), so this should not be used for release builds. No copy is performed when "
            .." If this option results in a SIGSEGV on startup, try loading a newer version of "
            .." libtsan with LD_PRELOAD."
    }

    newoption {
        trigger     = "compiler-toolset",
        description = "(Optional) Specify a compiler toolset (supported: clang (default MacOS), "
            .." gcc (default Linux), msc (default Windows)"
    }

    newoption {
        trigger     = "enable-gcov",
        description = "(Optional) enable GCC code coverage tracking, if supported."
    }

    newoption {
        trigger     = "linux-x86_64-cxx11-abi",
        description = "(Optional) Specify whether the C++11 ABI will be used. "
            .." This is set to false by default for CentOS7 compatibility."
    }

    newoption {
        trigger     = "winsdk-version",
        description = "(Optional) Specify the Windows SDK version on Microsoft hosts "
        .. "that do not use Packman winsdk packages."
    }

    newoption {
        trigger     = "visual-cxx-version",
        description = "(Optional) Specify the Visual C++ version on Microsoft hosts "
        .. "that do not use Packman MSVC packages."
    }

    newoption {
        trigger = "solution-name",
        description = "Solution name"
    }

end


-- Configure a workspace with the Omniverse defaults.
-- This function is intended to be run in a premake workspace.
-- This will set up the premake workspace to add all standard platforms and configurations for
-- Omniverse projects.
-- This is intended to be called inside your main project workspace.
-- @args.windows_x86_64_enabled: (optional, defaults to true) Can be used to enable/disable
--     windows-x86_64 as a target.
--     If this target is not disabled, @args.msvc_version and @args.winsdk_version must not be nil.
-- @args.linux_x86_64_enabled: (optional, defaults to true) Can be used to enable/disable
--     linux-x86_64 as a target.
-- @args.linux_aarch64_enabled: (optional, defaults to true) Can be used to enable/disable
--     linux-aarch64 as a target.
-- @args.macos_universal_enabled: (optional, defaults to true) Can be used to enable/disable
--     macos-universal as a target.
-- @args.msvc_version: (optional) The version of your msvc toolchain.
--     This must be specified if you are targeting windows-x86_64.
-- @args.winsdk_version: (optional) The version of your windows sdk.
--     This must be specified if you are targeting windows-x86_64.
-- @args.copy_windows_debug_libs: (Optional, defaults to true) Set to false to disable copying
--     vcruntime140d.dll and ucrtbased.dll into the debug target directory.
--     Windows agents running debug code without Visual Studio need these DLLs to run if they are
--     not using a static runtime.
--     These DLLs are not supposed to be redistributed publicly, so you need to exclude them from
--     packaging.
-- @args.use_pch: (optional, defaults to false) Set this to true if you're using PCH. If not, this
--     will specify the NoPCH flag.
-- @args.extra_warnings: (optional, defaults to false) Set this to true to enable additional warnings.
-- @args.security_hardening: (optional, defaults to false) Set to true to add security hardening settings.
-- @args.use_stack_clash_protection (optional, defaults to true) If @args.security_hardening is true,
--     enable -fstack-clash-protection. This option may not work on older compilers.
-- @args.host_deps_dir: (Optional) Where the host deps dir is located.
-- @args.target_deps_dir: (Optional) Where the target deps dir is located.
-- @args.allow_undefined_symbols_linux: (Optional) Set to true to allow modules to be linked with
--     undefined symbols on Linux. In most cases it is bad to set this to true but some projects
--     require this to build.
-- @args.linux_linker (optional, defaults to "ld") Allows you to switch the linux linker between
--                    "ld" and "gold". "gold" performs better so it's recommended.
--                    Note that gold uses DT_RUNPATH instead of DT_RPATH by default, so switching to
--                    gold may cause libraries to no longer be found at runtime unless you add
--                    `linkoptions{"-Wl,--disable-new-dtags"}`. You will likely already have this
--                    option set on linux-aarch64 if you rely on DT_RPATH.
-- @args.linux_gcc7_warnings (optional, defaults to true) Disables a slew of warnings-as-errors that
--                    became default between gcc7 and gcc11. Will default to false in the future.
-- @args.fix_cpp_version: (Optional) Add the `/Zc:__cplusplus` build option.
--                                   This will fix the `__cplusplus` definition on windows.
--                                   Without this option, `__cplusplus` is always set to `199711`.
--                                   This option will become the default behavior eventually, so a
--                                   warning is issued if this is not set.
--
-- @returns nil
function m.setup_workspace(args)
    local args = args or {}

    m._log("workspace setup")
    m.workspace_basics(args)

    -- Set up the supported platforms.
    if args.windows_x86_64_enabled == nil or args.windows_x86_64_enabled then
        m.add_windows_support(args)
    end

    if args.linux_x86_64_enabled == nil or args.linux_x86_64_enabled or args.linux_aarch64_enabled == nil or args.linux_aarch64_enabled then
        m.compiler_toolset_switch() -- switch the compiler, if specified
        m.add_linux_support(args)   -- this function internally checks args.linux_*_enabled
    end

    if args.macos_universal_enabled == nil or args.macos_universal_enabled then
        m.add_macos_universal_support()
    end

    m.ccache_compiler_options()    -- add ccache support if envvars are configured
    m.enable_sanitizers(args)      -- add UBSAN, ASAN, etc. based on premake flags
    m.set_default_configurations() -- setup debug/release modes
    if args ~= nil and args.extra_warnings then
        m.enable_extra_warnings(args)
    end
    if args ~= nil and args.security_hardening then
        m.add_security_hardening_flags(args)
    end
end

-- prints a message
-- @str: The message to print.
-- @returns nil.
function m._log(str)
    print("[repo_build (lua)] "..str)
end

-- Retrieve the platform token.
-- This has premake tokens in it so this refers to all platforms in a multi-platform configuration.
-- @returns The platform token.
function m.platform()
    -- we don't cross-compile on Mac
    if string.find(_OPTIONS["platform-host"], "macos") then
        return "macos-%{cfg.platform}"
    else
        return "%{cfg.system}-%{cfg.platform}"
    end
end


-- Retrieve the directory where each platform's build results are stored.
-- This has premake tokens in it so this refers to all platforms in a multi-platform configuration.
-- @returns The directory where each platform's build results are stored.
function m.platform_dir()
    return path.join(m.root, "_build", m.platform())
end


-- Retrieve the directory where each individual build target is stored.
-- This has premake tokens in it so it refers to all build targets.
-- @returns The directory where build results are stored for each target.
function m.target_dir()
    return path.join(m.platform_dir(), "%{cfg.buildcfg}")
end


-- Retrieve the name of the build tool that's being generated (e.g. gmake2, vs2019, etc.)
-- @returns The name of the build tool that's being generated.
function m.target_name()
    return _ACTION
end


-- Retrieve the directory where solution files are stored.
-- @returns The directory where solution files are stored.
function m.workspace_dir()
    return path.join(m.root, "_compiler", m.target_name())
end


-- Retrieve the directory where object files are stored.
-- @returns The directory where object files are stored.
function m.object_dir()
    return path.join(m.root, "_build/intermediate/%{cfg.system}/%{prj.name}")
end


-- Retrieve the host-deps directory.
-- @args.host_deps_dir: (Optional) Where the host deps dir is located.
-- @returns The host-deps directory.
function m.host_deps_dir(args)
    if args ~= nil and args.host_deps_dir ~= nil then
        return args.host_deps_dir
    else
        return path.join(m.root, "_build/host-deps")
    end
end


-- Retrieve the target-deps directory.
-- @args.target_deps_dir: (Optional) Where the target deps dir is located.
-- @returns The target-deps directory.
function m.target_deps_dir(args)
    if args ~= nil and args.target_deps_dir ~= nil then
        return args.target_deps_dir
    else
        return path.join(m.root, "_build/target-deps")
    end
end

-- Allow compiler toolset to be switched with the --compiler-toolset argument.
-- @returns nil.
function m.compiler_toolset_switch()
    -- Allow compiler selection override
    local compilerToolset = _OPTIONS["compiler-toolset"]
    if compilerToolset == "clang" then
        local hostDepsDir = m.host_deps_dir(args)
        toolset (compilerToolset)
        filter { "toolset:clang" }
        -- Taken from the ccache section in this file.
        -- Directly editing to include the full path to the compilers
            premake.tools.clang.tools.cc = hostDepsDir.."/clang/bin/clang"
            premake.tools.clang.tools.cxx = hostDepsDir.."/clang/bin/clang++"
            premake.tools.clang.tools.ar = hostDepsDir.."/clang/bin/llvm-ar"
    end
end


-- Returns the active compiler name and version
function m.get_compiler_info()
    local compiler_toolset = _OPTIONS["compiler-toolset"]
    local toolset = _OPTIONS["cc"] or _ACTION or "gcc"
    local compiler = toolset
    local version = ""

    if toolset == "clang" or compiler_toolset == "clang" then
        local hostDepsDir = m.host_deps_dir(args)
        clang_bin = hostDepsDir.."/clang/bin/clang"
        compiler = "clang"
        version = os.outputof(clang_bin.." --version | head -n 1 | awk '{print $3}'")
        version = tonumber(version:match("^(%d+)"))
    elseif toolset == "gmake2" then
        compiler = "gcc"
        version = os.outputof("gcc -dumpversion")
        version = tonumber(version:match("^(%d+)"))
    elseif toolset == "msc" or toolset == "msbuild" or string.find(toolset, "msc") then
        -- Assume msbuild version can be inferred from Visual Studio version
        local vs_version = _ACTION
        if vs_version == "vs2017" then
            version = 15
        elseif vs_version == "vs2019" then
            version = 16
        elseif vs_version == "vs2022" then
            version = 17
        else
            version = "unknown"
        end
        compiler = "msbuild"
    else
        compiler = "unknown"
        version = "unknown"
    end

    return compiler, version
end


-- Set up defaults for a workspace
-- @args.use_pch: (optional) Set this to true if you're using PCH. If not, this will specify the NoPCH flag.
function m.workspace_basics(args)
    configurations { "debug", "release" }

    -- Set location for solution files
    location (m.workspace_dir())

    -- Set the build result directory
    targetdir (m.target_dir())

    -- Location for intermediate files
    objdir (m.object_dir())

    -- defaults
    symbols "On"
    exceptionhandling "Off"
    rtti "Off"
    staticruntime "Off"

    if not _OPTIONS["no-fatal-warnings"] then
        flags { "FatalCompileWarnings" }
    end

    flags { "MultiProcessorCompile", "UndefinedIdentifiers", "NoIncrementalLink" }

    if args ~= nil and args.use_pch == nil and not args.use_pch then
        flags { "NoPCH" }
    end

    cppdialect "C++17"
end

-- Add support for x86_64 build targets.
-- @returns nil.
function m.add_x86_64_support()
    filter { "platforms:x86_64" }
        architecture "x86_64"
    filter {}
end


-- Add support for aarch64 build targets.
-- @returns nil.
function m.add_aarch64_support()
    filter { "platforms:aarch64" }
        architecture "ARM64"
    filter {}
end


-- Add support for Windows build targets.
-- This will only target x86_64.
-- @args.host_deps_dir: (Optional) Where the host deps dir is located.
-- @args.target_deps_dir: (Optional) Where the target deps dir is located.
-- @args.copy_windows_debug_libs: (Optional) Set to false to disable copying vcruntime140d.dll and
--                                ucrtbased.dll into the debug target directory.
--                                Windows agents running debug code without Visual Studio need these
--                                DLLs to run if they are not using a static runtime.
--                                These DLLs are not supposed to be redistributed publicly, so you
--                                need to exclude them from packaging.
-- @args.fix_cpp_version: (Optional) Add the `/Zc:__cplusplus` build option.
--                                   This will fix the `__cplusplus` definition on windows.
--                                   Without this option, `__cplusplus` is always set to `199711`.
--                                   This option will become the default behavior eventually, so a
--                                   warning is issued if this is not set.
-- @returns nil.
function m.add_windows_support(args)
    m._log("adding windows-x86_64 support")
    m.add_x86_64_support()
    m._setup_msvc_toolchain(args)

    filter { "system:windows" }
        platforms { "x86_64" }

        editandcontinue "Off"
        symbols "Full" -- only works in VS2017 or newer

        buildoptions {
            "/utf-8", -- all of our source strings and executable strings are utf8
            "/bigobj", -- TODO: why?
            "/permissive-", -- TODO: why?
            "/experimental:external", -- silence warnings from "/external:W" on some VS2019 toolchains.
        }
        if args.fix_cpp_version ~= nil and args.fix_cpp_version then
            buildoptions {
                "/Zc:__cplusplus", -- Fix __cplusplus definition (always set to 199711 without this).
            }
        else
            m._log("WARNING: fix_cpp_version was not set to true in repo_build.setup_workspace()")
        end

        -- add .editorconfig to all projects so that VS 2017 automatically picks it up
        files {".editorconfig"}
    filter {}
end


-- Add support for POSIX build targets.
-- This is used internally as common code for targets with GCC-like build commands.
-- @returns nil.
function m._add_posix_support()
    filter { "system:linux or macosx" }
        -- -pthread needs to be added in any multi-threaded code, which is everything in omniverse
        buildoptions { "-pthread" }

        -- vla is added because it's not standard C++ and not supported by MSVC
        enablewarnings { "all", "vla" }
    filter { "system:linux or macosx", "configurations:debug" }
        -- Stack protector to detect stack smashing bugs in debug builds.
        buildoptions { "-fstack-protector-strong" }
    filter {}
end


-- Add support for A Linux build target.
-- @args.linux_x86_64_enabled: (optional, defaults to true) Can be used to enable/disable
--                             linux-x86_64 as a target.
-- @args.linux_aarch64_enabled: (optional, defaults to true) Can be used to enable/disable
--                              linux-aarch64 as a target.
-- @args.linux_linker (optional, defaults to "ld") Allows you to switch the linux linker between
--                    "ld" and "gold". "gold" performs better so it's recommended.
-- @args.linux_gcc7_warnings (optional, defaults to true) Disables a slew of warnings-as-errors that
--                    became default between gcc7 and gcc11. Will default to false in the future.
-- @returns nil.
function m.add_linux_support(args)
    local args = args or {}

    m._add_posix_support{}

    local x86_64_enabled = args.linux_x86_64_enabled == nil or args.linux_x86_64_enabled
    local aarch64_enabled = args.linux_aarch64_enabled == nil or args.linux_aarch64_enabled
    local use_gcc7_warnings = args.linux_gcc7_warnings == nil or args.linux_gcc7_warnings

    if not x86_64_enabled and not aarch64_enabled then
        error("called add_linux_support() but x86_64 and aarch64 are disabled")
    end

    if x86_64_enabled then
        m._log("adding linux-x86_64 support")
        m.add_x86_64_support()
    end

    if aarch64_enabled then
        m._log("adding linux-aarch64 support")
        m.add_aarch64_support()
    end

    local compiler, compiler_version = m.get_compiler_info()
    m._log("Using compiler: " .. compiler .. ", version: " .. compiler_version)

    filter { "system:linux" }
        if x86_64_enabled then
            platforms {"x86_64"}
        end

        if aarch64_enabled then
            platforms {"aarch64"}
        end

        defines {
            "_FILE_OFFSET_BITS=64",
        }

        -- Fail to link if symbols are undefined.
        if not args.allow_undefined_symbols_linux then
            linkoptions { "-Wl,--no-undefined" }
        end

        if _OPTIONS["enable-gcov"] then
            buildoptions { "--coverage" }
            linkoptions { "--coverage" }
            defines { "OMNI_GCOV_BUILD" }
        end

        if _OPTIONS["compiler-toolset"] == "clang" then
            m._log("adding build options for clang")
            buildoptions { "-Xclang", "-flto-visibility-public-std" }
        end
        if args.linux_linker == "gold" then
            m._log("using gold linker")
            linkoptions { "-fuse-ld=gold" }
        end

        -- Disables a slew of new warnings-as-errors that were added between gcc7
        -- and gcc11.
        -- Projects can override individual warnings or change the value of
        -- linux_gcc7_warnings when calling repo_build.setup_workspace{}.
        if (use_gcc7_warnings == true) and
           ((compiler == "gcc" and compiler_version >= 11)) then
            m._log("Disabling new warnings-as-errors added after gcc 7 by default.")
            m._log("Set linux_gcc7_warnings to `false` to change default behavior.")
            buildoptions {
                "-Wno-error=alloca-larger-than=",
                "-Wno-error=attribute-alias",
                "-Wno-error=catch-value=",
                "-Wno-error=cast-function-type",
                "-Wno-error=class-memaccess",
                "-Wno-error=delete-incomplete",
                "-Wno-error=duplicated-branches",
                "-Wno-error=duplicated-cond",
                "-Wno-error=format-overflow",
                "-Wno-error=format-truncation",
                "-Wno-error=missing-attributes",
                "-Wno-error=null-dereference",
                "-Wno-error=nonnull",
                "-Wno-error=pessimizing-move",
                "-Wno-error=range-loop-construct",
                "-Wno-error=redundant-move",
                "-Wno-error=restrict",
                "-Wno-error=shadow",
                "-Wno-error=sizeof-pointer-div",
                "-Wno-error=stringop-overflow",
                "-Wno-error=stringop-truncation",
                "-Wno-error=suggest-attribute=format",
                "-Wno-error=switch",
                "-Wno-error=zero-length-bounds"
            }
        end

    filter { "system:linux", "platforms:x86_64" }
        if m.option_to_bool("linux-x86_64-cxx11-abi", false) then
            defines {
                "_GLIBCXX_USE_CXX11_ABI=1",
            }
        else
            defines {
                -- Omniverse has historically used the CentOS7 pre-C++11 ABI.
                -- This flag is not necessary in our CentOS7 docker but it's necessary when you build
                -- outside of it.
                "_GLIBCXX_USE_CXX11_ABI=0",
            }
        end

    filter { "system:linux", "configurations:release" }
        -- optimization
        linkoptions{"-Wl,-O1", "-Wl,--gc-sections"}
    filter {}
end


-- Add support for a macos-universal build target.
-- Note that premake refers to this platform as 'macosx' in filters
-- @returns: nil.
function m.add_macos_universal_support()
    m._log("adding macos-universal support")
    filter { "system:macosx" }
        platforms { "universal" }
    filter { "system:macosx", "platforms:universal" }
        -- cross compiler build options
        buildoptions { "-arch arm64", "-arch x86_64" }
        linkoptions { "-arch arm64", "-arch x86_64" }
    filter {}
end


-- Setup the default build configurations for Omniverse projects (debug and release).
-- @returns: nil.
function m.set_default_configurations()
    -- setup debug/release modes
    filter { "configurations:debug" }
        defines { "DEBUG" }
        -- `optimize "Debug"` exists for GCC, which passes -Og instead of -O0, but results in a lot of
        -- variables being optimized out, which adversely affects the debugging experience.
        optimize "Off"
    filter { "configurations:release" }
        defines { "NDEBUG" }
        -- gcov isn't meant to be used with optimization
        if _OPTIONS["enable-gcov"] then
            optimize "Off"
        else
            optimize "Speed"
        end
    filter {}
end

-- Library function to check if a path exists.
-- @path: The path to be checked
-- @returns: true if @path exists, false otherwise.
function m.path_exists(path)
    local file = io.open(path, "r")
    if file then
        file:close()
        return true
    else
        return false
    end
end

-- Set up the MSVC compiler toolchain on Windows.
-- This is run as part of setup(). This is not meant to be run as a standalone function.
-- This cannot be called prior to setup().
-- This must not be called inside a filter.
-- @args.host_deps_dir: (Optional) Where the host deps dir is located.
-- @args.target_deps_dir: (Optional) Where the target deps dir is located.
-- @args.copy_windows_debug_libs: (Optional) Set to false to disable copying vcruntime140d.dll and
--                                ucrtbased.dll into the debug target directory.
--                                Windows agents running debug code without Visual Studio need these
--                                DLLs to run if they are not using a static runtime.
--                                These DLLs are not supposed to be redistributed publicly, so you
--                                need to exclude them from packaging.
-- @returns nil
function m._setup_msvc_toolchain(args)
    -- Only execute with the premake target OS is Windows
    -- Otherwise in some projects args.msvc_version and args.winsdk_version are nil/None
    -- in a post-msvc/winsdk Packman package world.
    if os.target() ~= "windows" then
        return nil
    end

    -- Do not try to configure Windows compiler if no-compile action is set.
    if _ACTION == "no-compile" then
        return nil
    end

    local args = args or {}
    if _OPTIONS["visual-cxx-version"] ~= nil then
        args.msvc_version = _OPTIONS["visual-cxx-version"]
        m._log("Setting visual C++ target to repo build value of " .. _OPTIONS["visual-cxx-version"])
    end

    if _OPTIONS["winsdk-version"] ~= nil then
        args.winsdk_version = _OPTIONS["winsdk-version"]
        m._log("Setting winsdk target to repo build value of " .. _OPTIONS["winsdk-version"])
    end

    local compiler_toolset = _OPTIONS["compiler-toolset"]
    if compiler_toolset ~= nil and string.find(compiler_toolset, "msc%-v") then
        -- At some point this should be combined with `compiler_toolset_switch`
        m._log("Setting Visual Studio Build Tools version " .. compiler_toolset)
        toolset (compiler_toolset)
    end

    if args.msvc_version == nil then
        error("args.msvc_version was not specified. Cannot setup MSVC.")
    end

    if args.winsdk_version == nil then
        error("args.winsdk_version was not specified. Cannot setup MSVC.")
    end

    filter { "system:windows" }
        local host_deps = m.host_deps_dir(args)

        local msvc_base = host_deps.."/msvc/VC/Tools/MSVC/"..args.msvc_version
        local msvc_include = msvc_base.."/include"
        local msvc_libs = msvc_base.."/lib/onecore/x64"

        local winsdk_base = host_deps.."/winsdk"
        local winsdk_include = winsdk_base.."/include"
        local winsdk_lib = winsdk_base.."/lib"
        local sdk_include = {
            winsdk_include.."/winrt",
            winsdk_include.."/um",
            winsdk_include.."/ucrt",
            winsdk_include.."/shared"
        }
        local sdk_libs = {
            winsdk_lib.."/ucrt/x64",
            winsdk_lib.."/um/x64"
        }

        externalincludedirs { msvc_include, sdk_include }
        syslibdirs { msvc_libs, sdk_libs }
        bindirs {
            msvc_base.."/bin/HostX64/x64",
            host_deps.."/msvc/MSBuild/Current/Bin",
            winsdk_base.."/bin/x64"
        }

        systemversion (args.winsdk_version)

    filter {}


    if args.copy_windows_debug_libs == nil or args.copy_windows_debug_libs then
        m._log("Copying vcruntime140d.dll and ucrtbased.dll to the windows-x86_64/debug target directory. They must not be redistributed.")

        -- lua does not support globbing, and the underlying repo_man function for prebuild_copy
        -- doesn't support sane globbing.
        -- until the build tools version is always mapped in, this will have to do.
        local visual_c_versions = {"VC140", "VC141", "VC142", "VC143", "VC144", "VC145"}
        for idx, vc in ipairs(visual_c_versions) do
            local vcruntime_path = host_deps.."/msvc/VC/Redist/MSVC/" .. args.msvc_version .. "/debug_nonredist/x64/Microsoft." .. vc .. ".DebugCRT/vcruntime140d.dll" 
            if m.path_exists(vcruntime_path) then
                m.prebuild_copy(
                    {
                        {vcruntime_path, m.target_dir(args)},
                    },
                    "windows-x86_64.debug"
                )
            end

        end

        -- Packman winsdk has slightly different pathing to ucrtbased.dll vs
        -- the Microsoft winsdk folder structure.
        local winsdk_ucrt_paths = {winsdk_base.."/bin/x64/ucrt/ucrtbased.dll", winsdk_base.."/x64/ucrt/ucrtbased.dll"}
        for idx, ucrt_path in ipairs(winsdk_ucrt_paths) do
            if m.path_exists(ucrt_path) then
                m.prebuild_copy(
                    {
                        {ucrt_path, m.target_dir(args)},
                    },
                    "windows-x86_64.debug"
                )
            end
        end
    end
end


-- Enable support for ASAN, UBSAN, etc. when parameters are passed to premake.
-- @args.host_deps_dir: (Optional) Where the host deps dir is located.
-- @returns nil
function m.enable_sanitizers(args)
    if _OPTIONS["enable-ubsan"] then
        filter {"system:macosx or linux"}
            -- vptr sanitization is disabled because that warning triggers when
            -- a vptr from a module without RTTI is read by a module with RTTI
            -- (such as any python binding).
            buildoptions { "-fsanitize=undefined", "-fno-sanitize=vptr" }
            linkoptions { "-fsanitize=undefined" }
        filter {"system:linux"}
            linkoptions { "-static-libubsan" }
        filter{}
    end

    if _OPTIONS["enable-asan"] then
        -- this can't be linked statically (see GCC bug 64234), so this won't
        -- be available until we move away from docker
        filter { "system:windows" }
            buildoptions { "/fsanitize=address" }
            flags { "NoRuntimeChecks" }
            staticruntime "Off"
            if args ~= nil and args.msvc_version ~= nil then
                m.copy_to_targetdir(m.host_deps_dir(args).."/msvc/VC/Tools/MSVC/"..args.msvc_version.."/bin/HostX64/x64/clang_rt.asan*_dynamic-x86_64.dll")
            end
            -- ASan requires dynamic runtimes, so we *should* be using dynamic-forge, but carb.audio-forge.plugin will
            -- not link with staticruntime "Off" due to other third-party libs that require static runtimes.
            -- _OPTIONS["dynamic-forge"] = true
        filter {"system:linux or macosx"}
            buildoptions { "-fsanitize=address" }
            linkoptions { "-fsanitize=address" }
        filter {"system:linux"}
            if os.getenv("LINBUILD_EMBEDDED") then
                -- copy the library into the target directory
                -- the libasan.so.4 symlink can't be copied because repeatedly
                -- copying the symlink will cause a build failure
                postbuildcommands {"{COPY} /usr/lib64/libasan.so.4.0.0 %{cfg.targetdir}/libasan.so.4"}
            end

            -- asan instrumentation causes a few questionable looking warnings
            disablewarnings {
                "error"
            }
        filter{}
    end

    if _OPTIONS["enable-lsan"] then
        filter {"system:macosx or linux"}
            buildoptions { "-fsanitize=leak" }
            linkoptions { "-fsanitize=leak" }
        filter {"system:linux"}
            linkoptions { "-static-liblsan" }
        filter{}
    end

    if _OPTIONS["enable-tsan"] then
        filter {"system:macosx or linux"}
            buildoptions { "-fsanitize=thread" }
            linkoptions { "-fsanitize=thread" }
        filter {"system:linux"}
            -- this fails when linking statically for unknown reasons (probably a GCC bug)
            if os.getenv("LINBUILD_EMBEDDED") then
                -- copy the libraries into the target directory
                postbuildcommands {"{COPY} /usr/lib64/libtsan.so.0.0.0 %{cfg.targetdir}/libtsan.so.0"}
            end
        filter{}
    end
end

-- Enable extra warnings.
function m.enable_extra_warnings(args)
    m._log("setting warning level to 'Extra'")
    warnings "Extra"

    -- TODO: Add even more warnings or a default error profile.
    --       When we add things here, hide the new behavior behind a parameter. The default
    --       behavior can be deprecated (with a warning) and eventually removed if desired.
end


-- Add some build options that harden the binary against code injection.
-- These options have a minimal runtime cost but improve security somewhat.
-- @args.use_stack_clash_protection (optional, defaults to true) Enable -fstack-clash-protection.
--     This option may not work on older compilers.
-- @returns nil.
function m.add_security_hardening_flags(args)
    m._log("adding security hardening options")
    filter {"system:windows"}
        -- OVCC-1468: https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
        buildoptions { "/guard:cf" }
    filter {"system:macosx or linux"}
        buildoptions { "-fstack-protector-strong" }
    filter {"system:linux"}
        linkoptions { "-Wl,-z,relro", "-Wl,-z,now" }
    filter {"system:linux", "platforms:x86_64" }
        if args == nil or args.use_stack_clash_protection == nil or args.use_stack_clash_protection then
            buildoptions { "-fstack-clash-protection" }
        end
    filter{}
end


-- Build a python command that can be launched as pre/post build command.
-- @args.cmd A table containing the command you want to execute.
--           Note that the `%{root}` token does not work in pre/post build commands.
-- @args.python (optional) The path to the python interpreter. Packman's python is used by default.
-- @returns A string that can be executed with prebuildcommands() or postbuildcommands()
function m.python_cmd(args)
    if args == nil then
        error("python_cmd() called with nil args")
    end

    if args.cmd == nil then
        error("args.cmd cannot be nil")
    end

    if m.root == "%{root}" then
        error("repo_build.root cannot be set to the default (%{root}) when using repo_build.python_cmd")
    end

    local cmd = ""
    local python_launcher = args.python
    if python_launcher == nil then
        python_launcher = m.root.."/tools/packman/python"..m.shell_ext()

        if not os.isfile(python_launcher) then
            error("python launcher '"..python_launcher.."' does not exist")
        end
    end

    cmd = '"'..python_launcher..'"'

    for k, v in ipairs(args.cmd) do
        if m.is_host_windows() then
            -- escape batch envvars (e.g. %VAR%) as well as double quotes
            cmd = cmd..' "'..string.gsub(string.gsub(v, "%%", "%%%%"), '"', '""')..'"'
        elseif m.is_host_posix() then
            -- escape single quotes and Makefile variables.
            cmd = cmd.." '"..string.gsub(string.gsub(v, "'", "'\"'\"'"), "[$]", "$$").."'"
        else
            m.unknown_host_platform()
        end
    end

    --m._log("generated command: "..cmd)
    return cmd
end


function m.dofile_arg(filename, ...)
    local f = assert(loadfile(filename))
    return f(...)
end

-- Wrapper funcion around path.getabsolute() which makes drive letter lowercase on windows.
-- Otherwise drive letter can alter the case depending on environment and cause solution to reload.
-- @p: The input path.
-- @returns: The absolute path corresponding to p.
function m.get_abs_path(p)
    p = path.getabsolute(p)
    if os.target() == "windows" then
        p = p:gsub("^%a:", function(c) return c:lower() end)
    end
    return p
end

-- Copy a file to a directory using postbuildcommands().
-- Note that this will *only* copy a file when the corresponding project is rebuilt.
-- If the file copy fails, the copy will not be attempted on the next incremental build.
-- @filePath: The file to copy.
-- @dir: The destination to copy the file to.
-- @returns: nil
function m.copy_to_dir(filePath, dir)
    local filePathAbs = m.get_abs_path(filePath)
    postbuildcommands ("{MKDIR} " .. dir)
    if os.target() == "windows" then
        postbuildcommands {"{COPY} " .. filePathAbs .. " " .. dir .. "/"}
    else
        -- premake translates "{COPY}" to "cp -rf", which doesn't preserve symlinks
        -- Use "cp -Rf" instead
        postbuildcommands {"cp -Rf " .. filePathAbs .. " " .. dir .. "/"}
    end
end

-- Equivalent to copy_to_dir with %{cfg.targetdir} as the second argument.
-- %{cfg.targetdir} is _build/${platform}/${config}.
function m.copy_to_targetdir(filePath)
    m.copy_to_dir(filePath, "%{cfg.targetdir}")
end

-- Retrieve the path to the ccache directory.
-- This uses the CARB_CCACHE envvar to find the ccache directory.
-- @returns: The path to the ccache directory, if there is one.
-- @returns: nil otherwise.
function m.ccache_path()
    local candidateCcache = os.getenv("CARB_CCACHE")
    if candidateCcache ~= nil then
        local status = false
        cmdline = candidateCcache .. ' --version '
        if os.target() == "windows" then
            cmdline = cmdline .. '> nul'
        else
            cmdline = cmdline .. '> /dev/null'
        end
        status = os.execute(cmdline)
        if status == true then
            return candidateCcache
        end
    end
    return nil
end

-- OM-85491: Overrides the MSVC project generation to inject the following if ccache is enabled:
-- CLToolPath - this tells MSVC to look for `cl.exe` in the ccache folder, which is a copy
--              of ccache.exe itself. ccache will masquerade as the compiler itself, then
--              internally call MSVC's cl.exe from the PATH. repo_build handles this for us.
-- UseMultiToolTask - sets this to true. Needed or else ccache will fail to cache most
--              compilation results.
-- * To check if the project is missing cache hits, go to ccache_path(), and type "cl.exe -s"
premake.override(premake.vstudio.vc2010, "globals", function(base, cfg, toolset)
   local calls = base(cfg, toolset)
   if os.target() == "windows" and m.ccache_path() then
        -- Remove the executable name, we only need the path
        local ccache_folder = m.ccache_path():match("(.*\\)")
        -- Remove trailing backslash
        ccache_folder = ccache_folder.sub(ccache_folder, 1, -2)
        premake.push("<PropertyGroup>")
        premake.x("<CLToolPath>%s</CLToolPath>", ccache_folder)
        premake.x("<CLToolExe>cl.exe</CLToolExe>")
        premake.x("<UseMultiToolTask>true</UseMultiToolTask>")
        premake.pop("</PropertyGroup>")
   end
   return calls
end)

premake.override(premake.oven, "finishConfig", function (base, cfg)
    local calls = base(cfg)
    if cfg.project and cfg.system == "windows" then
        m.check_project_settings_for_ccache_conflicts(cfg.project)
    end
    return calls
end)

-- Injects ccache into the compiler toolchain if ccache is available.
-- This will conflict with anything else that calls gccprefix().
-- This is called by setup().
-- @returns nil
function m.ccache_compiler_options()
    -- From what I can tell we support clang and gcc on Linux, and clang on MacOS
    -- Providing broad checks against the tooling, gated by the user having set
    -- the `CARB_CCACHE` environment variable, or repo_build having done so.
    if m.ccache_path() then
        m._log("adding ccache support")
        filter { "toolset:gcc" }
            -- prepend ccache to the gccprefix so sneakily push all compiler
            -- calls through ccache.
            gccprefix (m.ccache_path().." ")
            -- Suggested compiler flag for ccache + PCH compatibility
            -- https://ccache.dev/manual/latest.html#_precompiled_headers
            buildoptions { "-fpch-preprocess" }
        filter { "toolset:clang" }
        -- Hack in ccache by directly editing the toolchain's variables.
        -- The only proper way to fix this would be to write a custom
        -- toolchain or add a new feature to premake to support ccache.
            premake.tools.clang.tools.cc = m.ccache_path().." "..premake.tools.clang.tools.cc
            premake.tools.clang.tools.cxx = m.ccache_path().." "..premake.tools.clang.tools.cxx
            -- Suggested compiler flag for ccache + PCH compatibility
            -- https://ccache.dev/manual/latest.html#_precompiled_headers
            buildoptions { "-Xclang -fno-pch-timestamp" }
        filter {}
    end
end

-- Split a string on separators.
-- @instr: The string to split.
-- @sep: The separator token to split on. This can be a character or string.
-- @returns: An array of substrings which contain the text in between the separators from instr.
function m.split(instr, sep)
    local substrings = {}; i = 1
    for str in string.gmatch(instr, "([^"..sep.."]+)") do
        substrings[i] = str
        i = i + 1
    end
    return substrings
end

-- Define the CARBONITE_BUILD macro which contains build information retrieved from git.
-- The macro contains the git hash, branch and whether it contains uncommitted modifications.
-- @returns: nil
function m.define_buildinfo()
    local version, versionErr = os.outputof("git --version")
    local hash, hashErr = os.outputof("git rev-parse --short HEAD")
    local branch, branchErr = os.outputof("git rev-parse --abbrev-ref HEAD")
    local dirty, dirtyErr = os.outputof("git status --porcelain")
    local buildInfo = "CARBONITE_BUILD=nogit nobranch DIRTY"

    if versionErr == 0 and hashErr == 0 and branchErr == 0 and dirtyErr == 0 then
        buildInfo = "CARBONITE_BUILD=" .. hash .. " " .. branch .. (dirty == "" and "" or " DIRTY")
    else
        m._log("Git errors! Is it installed on system?")
    end
    defines {buildInfo}
    m._log(buildInfo)
end

-- Use this function to change default location of generated prebuild file

-- Sets the location of the generated prebuild.toml file.
function m.set_prebuild_file(path)
    m.prebuild_file = io.open(path, 'w')
    m.prebuild_file_cache = {}
end

-- Write entry into generated prebuild file.
-- Premake style tokens like are replaced with python templates, e.g.: "%{root}" -> "${root}".
-- This will also modify repo_build.platform() with "${platform}" and %{cfg.buildcfg} with ${config}.
-- All relative paths are relative to currently executed lua file (like everything in premake).
-- @key: e.g. "copy", "link", "debug.link", "\"windows-x86_64.copy"\"
-- @data: table of pairs to link/copy, e.g.:
--          { { "copy/from", "copy/to" }, { "copy/from/*.py", "copy/here" }}
-- @returns: nil
function m.prebuild(key, data)
    -- If only one pair passed wrap into table and call again
    if data[1] and type(data[1]) == "string" then
        m.prebuild(key, {data})
        return
    end

    -- Default prebuild file
    if not m.prebuild_file then
        m.set_prebuild_file('_build/generated/prebuild.toml')
    end

    function process_path(p)
        -- FIXME: this is duplicated from m.platform(). Trying to clean this up with gsub had a
        -- massive perf hit, so we need to duplicate the code.
        -- Replace platform tokens because they don't have an exact repo_man equivalent
        -- % and - are magic characters in gsub(), so we need to escape them.
        local sanitized_platform = "%%{cfg.system}[-]%%{cfg.platform}"
        if string.find(_OPTIONS["platform-host"], "macos") then
            sanitized_platform = "macos[-]%%{cfg.platform}"
        end

        p = string.gsub(p, sanitized_platform, "${platform}")

        p = string.gsub(p, "%%{cfg.buildcfg}", "${config}")

        -- Replace premake style tokens like "%{root}" to python templates: "${root}"
        p = string.gsub(p, "%%{(.-)}", "${%1}")

        -- Relative path are relative to currently executed lua file (like everything in premake)
        if not path.isabsolute(p) and not string.startswith(p, "$") then
            p = path.getabsolute(p)
        end
        return p
    end

    -- Assume data is table of pairs:
    local headerWritten = false
    for _,entry in pairs(data) do
        if entry[1] and entry[2] then
            local path_from = process_path(entry[1])
            local path_to = process_path(entry[2])

            -- Write to file. Protect with cache to avoid writing the same multiple times:
            local line = "\t[\""..path_from.."\", \""..path_to.."\"],\n"
            local cache_key = key.."+"..line
            if m.prebuild_file_cache[cache_key] == nil then
                -- don't write out the header for this item unless something will actually be
                -- written to the file (ie: this entry doesn't already appear in the file).
                if not headerWritten then
                    m.prebuild_file:write("[[root]]\n")
                    m.prebuild_file:write(key.." = [\n")
                    headerWritten = true
                end

                m.prebuild_file:write(line)
                m.prebuild_file_cache[cache_key] = true
            end
        end
    end
    if headerWritten then
        m.prebuild_file:write("]\n\n")
    end
end

-- Write copy entry into generated prebuild file.
-- This adds a copy to the prebuild step of repo_build, which will be executed during the prebuild
-- section of every build. These copies have timestamp checking, so copies won't be performed if the
-- source file is older than the destination.
-- @data: table of pairs to copy, e.g.:
--          { { "copy/from", "copy/to" }, { "copy/from/*.py", "copy/here" }}
-- @filter: optional filters, like "debug", "\"windows-x86_64"\", "linux.release"
-- @returns: nil
function m.prebuild_copy(data, filter)
    local cmd = (filter and filter.."." or "").."copy"
    m.prebuild(cmd, data)
end


-- Write link entry into generated prebuild file.
-- This adds a link to the prebuild step of repo_build, which will be executed during the prebuild
-- section of every build.
-- @data: table of pairs to link, e.g.:
--          { { "link/from", "link/to" }, { "link/from/*.py", "link/here" }}
-- @filter: optional filters, like "debug", "\"windows-x86_64"\", "linux.release"
-- @returns: nil
function m.prebuild_link(data, filter)
    local cmd = (filter and filter.."." or "").."link"
    m.prebuild(cmd, data)
end


local sourcelink = require('build/sourcelink')

function m.enable_vstudio_sourcelink()
    premake.override(premake.vstudio.vc2010, "additionalLinkOptions", function(base, cfg)
        sourcelink.sourcelink(cfg)
        return base(cfg)
    end)
end

function m.remove_vstudio_jmc()
    premake.override(premake.vstudio.vc2010.elements, "clCompile", function(oldfn, cfg)
        local calls = oldfn(cfg)
        table.insert(calls, function(cfg)
            premake.vstudio.vc2010.element("SupportJustMyCode", nil, "false")
        end)
        return calls
    end)
end

-- Common python bindings settings.
-- @name: The name of the target being built.
-- @python_folder: The directory of your python installation.
-- @python_version: The version of python that's being built against.
-- @config: Which config the bindings are being built against. This can be nil for both.
-- @returns: nil
function m.define_bindings_python(name, python_folder, python_version, config)

    local python_version = python_version or "3.6"

    python_version_specific_settings = {
        ["3.6"] = function ()
            filter { "system:windows" }
                -- Special naming according to https://www.python.org/dev/peps/pep-0425/
                targetname (name..".cp36-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-36m-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-36m-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.6m" }
                links { python_folder.."/lib/python3.6m" }
            filter {}
        end,

        ["3.7"] = function ()
            filter { "system:windows" }
                targetname (name..".cp37-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-37m-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-37m-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.7m" }
                links { python_folder.."/lib/python3.7m" }
            filter {}
        end,

        ["3.8"] = function ()
            filter { "system:windows" }
                targetname (name..".cp38-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-38-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-38-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.8" }
                links { python_folder.."/lib/python3.8" }
            filter { "system:macosx" }
                targetname (name..".cpython-38-darwin")
                includedirs { python_folder.."/include/python3.8" }
                links { python_folder.."/lib/python3.8" }
            filter {}
        end,

        ["3.9"] = function ()
            filter { "system:windows" }
                targetname (name..".cp39-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-39-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-39-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.9" }
                links { python_folder.."/lib/python3.9" }
            filter { "system:macosx" }
                targetname (name..".cpython-39-darwin")
                includedirs { python_folder.."/include/python3.9" }
                links { python_folder.."/lib/python3.9" }
            filter {}
        end,

        ["3.10"] = function ()
            filter { "system:windows" }
                targetname (name..".cp310-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-310-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-310-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.10" }
                links { python_folder.."/lib/python3.10" }
            filter { "system:macosx" }
                targetname (name..".cpython-310-darwin")
                includedirs { python_folder.."/include/python3.10" }
                links { python_folder.."/lib/python3.10" }
            filter {}
        end,

        ["3.11"] = function ()
            filter { "system:windows" }
                targetname (name..".cp311-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-311-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-311-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.11" }
                links { python_folder.."/lib/python3.11" }
            filter { "system:macosx" }
                targetname (name..".cpython-311-darwin")
                includedirs { python_folder.."/include/python3.11" }
                links { python_folder.."/lib/python3.11" }
            filter {}
        end,

        ["3.12"] = function ()
            filter { "system:windows" }
                targetname (name..".cp312-win_amd64")
            filter { "system:linux", "platforms:x86_64" }
                targetname (name..".cpython-312-x86_64-linux-gnu")
            filter { "system:linux", "platforms:aarch64" }
                targetname (name..".cpython-312-aarch64-linux-gnu")
            filter { "system:linux" }
                includedirs { python_folder.."/include/python3.12" }
                links { python_folder.."/lib/python3.12" }
            filter { "system:macosx" }
                targetname (name..".cpython-312-darwin")
                includedirs { python_folder.."/include/python3.12" }
                links { python_folder.."/lib/python3.12" }
            filter {}
        end,
    }

    kind "SharedLib"

    -- Pybind11 needs exceptions and rtti:
    exceptionhandling "On"
    rtti "On"

    -- Python bindings use dynamic runtime because of pybind internal design. It is important to use the same runtime
    -- for all bindings. We use release/debug runtime depending on configuration, which is default behavior.
    -- carb.scrpting-python.plugin should do the same.
    staticruntime "Off"

    if config and (config == "debug" or config == "release") then
        if config == "debug" then
            runtime "Debug"
            -- pybind11 uses this define to build debug configuration:
            defines {"_DEBUG"}
        end
    else
        filter { "configurations:debug" }
            runtime "Debug"
            -- pybind11 uses this define to build debug configuration:
            defines {"_DEBUG"}
        filter{}
    end
    filter { "system:windows" }
        buildoptions { "/bigobj" }

        -- Python
        libdirs { python_folder.."/libs" }
        includedirs { python_folder.."/include" }

        targetextension(".pyd")

    filter { "system:linux" }
        targetprefix("")

        -- Show undefined symbols as linker errors
        linkoptions { "-Wl,--no-undefined" }

    filter { "system:macosx" }
        targetprefix("")
        targetextension(".so")
    filter {}

    local version_settings = python_version_specific_settings[python_version]
    if (version_settings == nil) then
        error ('No Python settings for `python_version` value '..tostring(python_version))
    end
    version_settings()
end

--- Silence retarget dialog
premake.override(premake.vstudio.vc2010.elements, "globals", function(base, prj)
    local calls = base(prj)
    table.insertafter(calls, premake.vstudio.vc2010.globals, function(prj)
        premake.w('<VCProjectUpgraderObjectName>NoUpgrade</VCProjectUpgraderObjectName>')
    end)
    return calls
end)

-- Get the directory of the lua file that this function was called from.
-- @returns The directory of the lua file that this function was called from.
function m.get_source_dir()
    -- :sub(2) to remove the leading @
    return path.getdirectory(debug.getinfo(2,'S').source):sub(2)
end

-- Check whether the host system is a given platform
-- @platform: (string) The platform name to check for. This can be a substring of the full platform.
--     (e.g. "windows" or "x86_64" for "windows-x86_64").
-- @returns: true if the host platform matches the platform string, false otherwise.
function m.query_host_platform(platform)
    return string.find(_OPTIONS["platform-host"], platform) ~= nil
end

-- Check if we're running on Windows.
-- @returns true if we're running on Windows.
function m.is_host_windows()
    return m.query_host_platform("windows")
end

-- Check if we're running on Linux.
-- @returns true if we're running on Linux.
function m.is_host_linux()
    return m.query_host_platform("linux")
end

-- Check if we're running on Mac OS.
-- @returns true if we're running on Mac OS.
function m.is_host_macos()
    return m.query_host_platform("macos")
end

-- Check if we're running on a POSIX host.
-- @returns true if we're running a POSIX host.
function m.is_host_posix()
    return m.is_host_linux() or m.is_host_macos()
end

-- This function should be called when encountering a host platform that the code can't handle.
-- This will error out of the premake script immediately.
-- To avoid portability issues, you should have an else branch with this call when you're switching
-- on host platform.
function m.unknown_host_platform()
    error("unknown host platform '".._OPTIONS["host-platform"].."'")
end

-- Query the shell extension for the host platform
-- @returns ".sh" on Linux and Mac, ".bat" on windows
function m.shell_ext()
    if m.is_host_posix() then
        return ".sh"
    elseif m.is_host_windows() then
        return ".bat"
    else
        error("unknown platform '".._OPTIONS["host-platform"])
    end
end

-- Set up a project to be a utility project so you can just run prebuild commands.
-- This function is a workaround for the fact that `kind "Utility"` doesn't work under Make.
function m.utility_project()
    if os.target() == "windows" then
        kind "Utility"
    else
        -- Utility projects don't work in make
        kind "StaticLib"
        files { m.get_source_dir().."/Empty.cpp" }
    end
end

-- Evaluate a command line option as a boolean value.
-- This allows you to specify 'true', 'yes' and non-zero number as a true value and 'false', 'no'
-- and '0' as a false value. Partial words can be specified and capitalization is ignored.
-- Unknown option values will result in a call to error().
-- @name: The name of the command line argument being evaluated. _OPTIONS is indexed by this.
-- @default: The default value to specify if that parameter was not specified.
-- @returns true/false depending on how the parameter evaluated.
-- @returns @default if the option was not specified.
function m.option_to_bool(name, default)
    true_patterns = {
        '^[tT][rR]?[uU]?[eE]?$',
        '^[yY][eE]?[sS]?$',
        '^[1-9][0-9]*$',
    }
    false_patterns = {
        '^[fF][aA]?[lL]?[sS]?[eE]?$',
        '^[nN][oO]?$',
        '^0$',
    }

    local opt = _OPTIONS[name]
    if opt == nil or opt == "" then
        return default
    end

    for k, v in ipairs(true_patterns) do
        if string.match(opt, v) then
            return true
        end
    end

    for k, v in ipairs(false_patterns) do
        if string.match(opt, v) then
            return false
        end
    end

    error("unknown value passed to option '"..name.."': '"..opt.."'")
end

-- * OM-94459 To maximize cache hits, a project will also need to:
--   * Not use "NoIncrementalLink"
--   * Not use the "editandcontinue" premake option
--   * Must use: debugformat "c7"  - PDBs built with the compiler are not supported by ccache
--   * Must use: symbols "On" or "Off", not "Full"
function m.check_project_settings_for_ccache_conflicts(prj)
    if prj.symbols == "Full" then
        m._log("WARNING: "..prj.name.." ccache conflicts checking: symbols On or Off, not Full.")
        return false
    end
    if prj.editandcontinue == "On" then
        m._log("WARNING: "..prj.name.." ccache conflicts checking: Not use editandcontinue.")
        return false
    end
    if prj.debugformat ~= "c7" then
        m._log("WARNING: "..prj.name.." ccache conflicts checking: debugformat must use c7.")
        return false
    end
    if prj.flags ~= nil then
        for _, v in pairs(prj.flags) do
            if v == "NoIncrementalLink" then
                m._log("WARNING: "..prj.name.." ccache conflicts checking: flags not use NoIncrementalLink.")
                return false
            end
        end
    end
    return true
end

return m