tools/corrosion/cmake/FindRust.cmake
author unC0Rr
Fri, 22 Nov 2024 17:29:30 +0100
branchtransitional_engine
changeset 16040 6c5b3c576fc6
parent 16021 6a3dc15b78b9
child 16038 d903f8d2395a
permissions -rw-r--r--
Add some progress on rust AI

#[=======================================================================[.rst:
FindRust
--------

Find Rust

This module finds an installed rustc compiler and the cargo build tool. If Rust
is managed by rustup it determines the available toolchains and returns a
concrete Rust version, not a rustup proxy.

#]=======================================================================]

cmake_minimum_required(VERSION 3.12)

# search for Cargo here and set up a bunch of cool flags and stuff
include(FindPackageHandleStandardArgs)

list(APPEND CMAKE_MESSAGE_CONTEXT "FindRust")

# Print error message and return.
macro(_findrust_failed)
    if("${Rust_FIND_REQUIRED}")
        message(FATAL_ERROR ${ARGN})
    elseif(NOT "${Rust_FIND_QUIETLY}")
        message(WARNING ${ARGN})
    endif()
    # Note: PARENT_SCOPE is the scope of the caller of the caller of this macro.
    set(Rust_FOUND "" PARENT_SCOPE)
    return()
endmacro()

# Checks if the actual version of a Rust toolchain matches the VERSION requirements specified in find_package.
function(_findrust_version_ok ACTUAL_VERSION OUT_IS_OK)
    if(DEFINED Rust_FIND_VERSION_RANGE)
        if(Rust_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE")
            set(COMPARSION_OPERATOR "VERSION_LESS_EQUAL")
        elseif(Rust_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE")
            set(COMPARSION_OPERATOR "VERSION_LESS")
        else()
            message(FATAL_ERROR "Unexpected value in `<PackageName>_FIND_VERSION_RANGE_MAX`: "
                    "`${Rust_FIND_VERSION_RANGE_MAX}`.")
        endif()
        if(("${ACTUAL_VERSION}" VERSION_GREATER_EQUAL "${Rust_FIND_VERSION_RANGE_MIN}")
                AND
            ( "${ACTUAL_VERSION}" ${COMPARSION_OPERATOR} "${Rust_FIND_VERSION_RANGE_MAX}" )
        )
            set("${OUT_IS_OK}" TRUE PARENT_SCOPE)
        else()
            set("${OUT_IS_OK}" FALSE PARENT_SCOPE)
        endif()
    elseif(DEFINED Rust_FIND_VERSION)
        if(Rust_VERSION_EXACT)
            set(COMPARISON_OPERATOR VERSION_EQUAL)
        else()
            set(COMPARISON_OPERATOR VERSION_GREATER_EQUAL)
        endif()
        if(_TOOLCHAIN_${_TOOLCHAIN_SELECTED}_VERSION "${COMPARISON_OPERATOR}" Rust_FIND_VERSION)
            set("${OUT_IS_OK}" TRUE PARENT_SCOPE)
        else()
            set("${OUT_IS_OK}" FALSE PARENT_SCOPE)
        endif()
    else()
        # if no VERSION requirement was specified, the version is always okay.
        set("${OUT_IS_OK}" TRUE PARENT_SCOPE)
    endif()
endfunction()

function(_corrosion_strip_target_triple input_triple_or_path output_triple)
    # If the target_triple is a path to a custom target specification file, then strip everything
    # except the filename from `target_triple`.
    get_filename_component(target_triple_ext "${input_triple_or_path}" EXT)
    set(target_triple "${input_triple_or_path}")
    if(target_triple_ext)
        if(target_triple_ext STREQUAL ".json")
            get_filename_component(target_triple "${input_triple_or_path}"  NAME_WE)
        endif()
    endif()
    set(${output_triple} "${target_triple}" PARENT_SCOPE)
endfunction()

function(_corrosion_parse_target_triple target_triple out_arch out_vendor out_os out_env)
    _corrosion_strip_target_triple(${target_triple} target_triple)

    # The vendor part may be left out from the target triple, and since `env` is also optional,
    # we determine if vendor is present by matching against a list of known vendors.
    set(known_vendors
        "apple"
        "esp[a-z0-9]*" # espressif, e.g. riscv32imc-esp-espidf or xtensa-esp32s3-none-elf
        "fortanix"
        "kmc"
        "pc"
        "nintendo"
        "nvidia"
        "openwrt"
        "alpine"
        "chimera"
        "unikraft"
        "unknown"
        "uwp" # aarch64-uwp-windows-msvc
        "wrs" # e.g. aarch64-wrs-vxworks
        "sony"
        "sun"
        )
    # todo: allow users to add additional vendors to the list via a cmake variable.
    list(JOIN known_vendors "|" known_vendors_joined)
    # vendor is optional - We detect if vendor is present by matching against a known list of
    # vendors. The next field is the OS, which we assume to always be present, while the last field
    # is again optional and contains the environment.
    string(REGEX MATCH
        "^([a-z0-9_\.]+)-((${known_vendors_joined})-)?([a-z0-9_]+)(-([a-z0-9_]+))?$"
        whole_match
        "${target_triple}"
        )
    if((NOT whole_match) AND (NOT CORROSION_NO_WARN_PARSE_TARGET_TRIPLE_FAILED))
        message(WARNING "Failed to parse target-triple `${target_triple}`."
            "Corrosion determines some information about the output artifacts based on OS "
            "specified in the Rust target-triple.\n"
            "Currently this is relevant for windows and darwin (mac) targets, since file "
            "extensions differ.\n"
            "Note: If you are targeting a different OS you can suppress this warning by"
            " setting the CMake cache variable "
            "`CORROSION_NO_WARN_PARSE_TARGET_TRIPLE_FAILED`."
            "Please consider opening an issue on github if you you need to add a new vendor to the list."
            )
    endif()

    message(DEBUG "Parsed Target triple: arch: ${CMAKE_MATCH_1}, vendor: ${CMAKE_MATCH_3}, "
        "OS: ${CMAKE_MATCH_4}, env: ${CMAKE_MATCH_6}")

    set("${out_arch}" "${CMAKE_MATCH_1}" PARENT_SCOPE)
    set("${out_vendor}" "${CMAKE_MATCH_3}" PARENT_SCOPE)
    set("${out_os}" "${CMAKE_MATCH_4}" PARENT_SCOPE)
    set("${out_env}" "${CMAKE_MATCH_6}" PARENT_SCOPE)
endfunction()

function(_corrosion_determine_libs_new target_triple out_libs out_flags)
    set(package_dir "${CMAKE_BINARY_DIR}/corrosion/required_libs")
    # Cleanup on reconfigure to get a cleans state (in case we change something in the future)
    file(REMOVE_RECURSE "${package_dir}")
    file(MAKE_DIRECTORY "${package_dir}")
    set(manifest "[package]\nname = \"required_libs\"\nedition = \"2018\"\nversion = \"0.1.0\"\n")
    string(APPEND manifest "\n[lib]\ncrate-type=[\"staticlib\"]\npath = \"lib.rs\"\n")
    string(APPEND manifest "\n[workspace]\n")
    file(WRITE "${package_dir}/Cargo.toml" "${manifest}")
    file(WRITE "${package_dir}/lib.rs" "pub fn add(left: usize, right: usize) -> usize {left + right}\n")

    execute_process(
        COMMAND ${CMAKE_COMMAND} -E env
            "CARGO_BUILD_RUSTC=${Rust_COMPILER_CACHED}"
        ${Rust_CARGO_CACHED} rustc --verbose --color never --target=${target_triple} -- --print=native-static-libs
        WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/corrosion/required_libs"
        RESULT_VARIABLE cargo_build_result
        ERROR_VARIABLE cargo_build_error_message
    )
    if(cargo_build_result)
        message(DEBUG "Determining required native libraries - failed: ${cargo_build_result}.")
        message(TRACE "The cargo build error was: ${cargo_build_error_message}")
        message(DEBUG "Note: This is expected for Rust targets without std support")
        return()
    else()
        # The pattern starts with `native-static-libs:` and goes to the end of the line.
        if(cargo_build_error_message MATCHES "native-static-libs: ([^\r\n]+)\r?\n")
            string(REPLACE " " ";" "libs_list" "${CMAKE_MATCH_1}")
            set(stripped_lib_list "")
            set(flag_list "")

            set(was_last_framework OFF)
            foreach(lib ${libs_list})
                # merge -framework;lib -> "-framework lib" as CMake does de-duplication of link libraries, and -framework prefix is required
                if (lib STREQUAL "-framework")
                    set(was_last_framework ON)
                    continue()
                endif()
                if (was_last_framework)
                    list(APPEND stripped_lib_list "-framework ${lib}")
                    set(was_last_framework OFF)
                    continue()
                endif()
                
                # Flags start with / for MSVC
                if (lib MATCHES "^/" AND ${target_triple} MATCHES "msvc$")
                    list(APPEND flag_list "${lib}")
                else()
                    # Strip leading `-l` (unix) and potential .lib suffix (windows)
                    string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}")
                    string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}")
                    list(APPEND stripped_lib_list "${stripped_lib}")
                endif()
            endforeach()
            set(libs_list "${stripped_lib_list}")
            # Special case `msvcrt` to link with the debug version in Debug mode.
            list(TRANSFORM libs_list REPLACE "^msvcrt$" "\$<\$<CONFIG:Debug>:msvcrtd>")
        else()
            message(DEBUG "Determining required native libraries - failed: Regex match failure.")
            message(DEBUG "`native-static-libs` not found in: `${cargo_build_error_message}`")
            return()
        endif()
    endif()
    set("${out_libs}" "${libs_list}" PARENT_SCOPE)
    set("${out_flags}" "${flag_list}" PARENT_SCOPE)
endfunction()

if (NOT "${Rust_TOOLCHAIN}" STREQUAL "$CACHE{Rust_TOOLCHAIN}")
    # Promote Rust_TOOLCHAIN to a cache variable if it is not already a cache variable
    set(Rust_TOOLCHAIN ${Rust_TOOLCHAIN} CACHE STRING "Requested rustup toolchain" FORCE)
endif()

set(_RESOLVE_RUSTUP_TOOLCHAINS_DESC "Indicates whether to descend into the toolchain pointed to by rustup")
set(Rust_RESOLVE_RUSTUP_TOOLCHAINS ON CACHE BOOL ${_RESOLVE_RUSTUP_TOOLCHAINS_DESC})

# This block checks to see if we're prioritizing a rustup-managed toolchain.
if (DEFINED Rust_TOOLCHAIN)
    # If the user specifies `Rust_TOOLCHAIN`, then look for `rustup` first, rather than `rustc`.
    find_program(Rust_RUSTUP rustup PATHS "$ENV{HOME}/.cargo/bin")
    if(NOT Rust_RUSTUP)
        if(NOT "${Rust_FIND_QUIETLY}")
            message(
                WARNING "CMake variable `Rust_TOOLCHAIN` specified, but `rustup` was not found. "
                "Ignoring toolchain and looking for a Rust toolchain not managed by rustup.")
        endif()
    endif()
else()
    # If we aren't definitely using a rustup toolchain, look for rustc first - the user may have
    # a toolchain installed via a method other than rustup higher in the PATH, which should be
    # preferred. However, if the first-found rustc is a rustup proxy, then we'll revert to
    # finding the preferred toolchain via rustup.

    # Uses `Rust_COMPILER` to let user-specified `rustc` win. But we will still "override" the
    # user's setting if it is pointing to `rustup`. Default rustup install path is provided as a
    # backup if a toolchain cannot be found in the user's PATH.

    if (DEFINED Rust_COMPILER)
        set(_Rust_COMPILER_TEST "${Rust_COMPILER}")
        set(_USER_SPECIFIED_RUSTC ON)
        if(NOT (EXISTS "${_Rust_COMPILER_TEST}" AND NOT IS_DIRECTORY "${_Rust_COMPILER_TEST}"))
            set(_ERROR_MESSAGE "Rust_COMPILER was set to `${Rust_COMPILER}`, but this file does "
                "not exist."
            )
            _findrust_failed(${_ERROR_MESSAGE})
            return()
        endif()
    else()
        find_program(_Rust_COMPILER_TEST rustc PATHS "$ENV{HOME}/.cargo/bin")
        if(NOT EXISTS "${_Rust_COMPILER_TEST}")
            set(_ERROR_MESSAGE "`rustc` not found in PATH or `$ENV{HOME}/.cargo/bin`.\n"
                    "Hint: Check if `rustc` is in PATH or manually specify the location "
                    "by setting `Rust_COMPILER` to the path to `rustc`.")
            _findrust_failed(${_ERROR_MESSAGE})
        endif()
    endif()

    # Check if the discovered rustc is actually a "rustup" proxy.
    execute_process(
        COMMAND
            ${CMAKE_COMMAND} -E env
                RUSTUP_FORCE_ARG0=rustup
            "${_Rust_COMPILER_TEST}" --version
        OUTPUT_VARIABLE _RUSTC_VERSION_RAW
        ERROR_VARIABLE _RUSTC_VERSION_STDERR
        RESULT_VARIABLE _RUSTC_VERSION_RESULT
    )

    if(NOT (_RUSTC_VERSION_RESULT EQUAL "0"))
        _findrust_failed("`${_Rust_COMPILER_TEST} --version` failed with ${_RUSTC_VERSION_RESULT}\n"
            "rustc stderr:\n${_RUSTC_VERSION_STDERR}"
            )
    endif()

    if (_RUSTC_VERSION_RAW MATCHES "rustup [0-9\\.]+")
        if (_USER_SPECIFIED_RUSTC)
            message(
                WARNING "User-specified Rust_COMPILER pointed to rustup's rustc proxy. Corrosion's "
                "FindRust will always try to evaluate to an actual Rust toolchain, and so the "
                "user-specified Rust_COMPILER will be discarded in favor of the default "
                "rustup-managed toolchain."
            )

            unset(Rust_COMPILER)
            unset(Rust_COMPILER CACHE)
        endif()

        # Get `rustup` next to the `rustc` proxy
        get_filename_component(_RUST_PROXIES_PATH "${_Rust_COMPILER_TEST}" DIRECTORY)
        find_program(Rust_RUSTUP rustup HINTS "${_RUST_PROXIES_PATH}" NO_DEFAULT_PATH)
    endif()

    unset(_Rust_COMPILER_TEST CACHE)
endif()

# At this point, the only thing we should have evaluated is a path to `rustup` _if that's what the
# best source for a Rust toolchain was determined to be_.
if (NOT Rust_RUSTUP)
    set(Rust_RESOLVE_RUSTUP_TOOLCHAINS OFF CACHE BOOL ${_RESOLVE_RUSTUP_TOOLCHAINS_DESC} FORCE)
endif()

# List of user variables that will override any toolchain-provided setting
set(_Rust_USER_VARS Rust_COMPILER Rust_CARGO Rust_CARGO_TARGET Rust_CARGO_HOST_TARGET)
foreach(_VAR ${_Rust_USER_VARS})
    if (DEFINED "${_VAR}")
        set(${_VAR}_CACHED "${${_VAR}}" CACHE INTERNAL "Internal cache of ${_VAR}")
    else()
        unset(${_VAR}_CACHED CACHE)
    endif()
endforeach()

# Discover what toolchains are installed by rustup, if the discovered `rustc` is a proxy from
# `rustup` and the user hasn't explicitly requested to override this behavior, then select either
# the default toolchain, or the requested toolchain Rust_TOOLCHAIN
if (Rust_RESOLVE_RUSTUP_TOOLCHAINS)
    execute_process(
        COMMAND
            "${Rust_RUSTUP}" toolchain list --verbose
        OUTPUT_VARIABLE _TOOLCHAINS_RAW
    )

    string(REPLACE "\n" ";" _TOOLCHAINS_RAW "${_TOOLCHAINS_RAW}")
    set(_DISCOVERED_TOOLCHAINS "")
    set(_DISCOVERED_TOOLCHAINS_RUSTC_PATH "")
    set(_DISCOVERED_TOOLCHAINS_CARGO_PATH "")
    set(_DISCOVERED_TOOLCHAINS_VERSION "")

    foreach(_TOOLCHAIN_RAW ${_TOOLCHAINS_RAW})
        if (_TOOLCHAIN_RAW MATCHES "([a-zA-Z0-9\\._\\-]+)[ \t\r\n]?(\\(default\\) \\(override\\)|\\(default\\)|\\(override\\))?[ \t\r\n]+(.+)")
            set(_TOOLCHAIN "${CMAKE_MATCH_1}")
            set(_TOOLCHAIN_TYPE "${CMAKE_MATCH_2}")

            set(_TOOLCHAIN_PATH "${CMAKE_MATCH_3}")
            set(_TOOLCHAIN_${_TOOLCHAIN}_PATH "${CMAKE_MATCH_3}")

            if (_TOOLCHAIN_TYPE MATCHES ".*\\(default\\).*")
                set(_TOOLCHAIN_DEFAULT "${_TOOLCHAIN}")
            endif()

            if (_TOOLCHAIN_TYPE MATCHES ".*\\(override\\).*")
                set(_TOOLCHAIN_OVERRIDE "${_TOOLCHAIN}")
            endif()

            execute_process(
                    COMMAND
                    "${_TOOLCHAIN_PATH}/bin/rustc" --version
                    OUTPUT_VARIABLE _TOOLCHAIN_RAW_VERSION
            )
            if (_TOOLCHAIN_RAW_VERSION MATCHES "rustc ([0-9]+)\\.([0-9]+)\\.([0-9]+)(-nightly)?")
                list(APPEND _DISCOVERED_TOOLCHAINS "${_TOOLCHAIN}")
                list(APPEND _DISCOVERED_TOOLCHAINS_RUSTC_PATH "${_TOOLCHAIN_PATH}/bin/rustc")
                list(APPEND _DISCOVERED_TOOLCHAINS_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")

                # We need this variable to determine the default toolchain, since `foreach(... IN ZIP_LISTS ...)`
                # requires CMake 3.17. As a workaround we define this variable to lookup the version when iterating
                # through the `_DISCOVERED_TOOLCHAINS` lists.
                set(_TOOLCHAIN_${_TOOLCHAIN}_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
                if(CMAKE_MATCH_4)
                    set(_TOOLCHAIN_${_TOOLCHAIN}_IS_NIGHTLY "TRUE")
                else()
                    set(_TOOLCHAIN_${_TOOLCHAIN}_IS_NIGHTLY "FALSE")
                endif()
                if(EXISTS "${_TOOLCHAIN_PATH}/bin/cargo")
                    list(APPEND _DISCOVERED_TOOLCHAINS_CARGO_PATH "${_TOOLCHAIN_PATH}/bin/cargo")
                else()
                    list(APPEND _DISCOVERED_TOOLCHAINS_CARGO_PATH "NOTFOUND")
                endif()
            else()
                message(AUTHOR_WARNING "Unexpected output from `rustc --version` for Toolchain `${_TOOLCHAIN}`: "
                        "`${_TOOLCHAIN_RAW_VERSION}`.\n"
                        "Ignoring this toolchain."
                )
            endif()
        else()
            message(AUTHOR_WARNING "Didn't recognize toolchain: ${_TOOLCHAIN_RAW}. Ignoring this toolchain.\n"
                "Rustup toolchain list output( `${Rust_RUSTUP} toolchain list --verbose`):\n"
                "${_TOOLCHAINS_RAW}"
            )
        endif()
    endforeach()

    # Expose a list of available rustup toolchains.
    list(LENGTH _DISCOVERED_TOOLCHAINS _toolchain_len)
    list(LENGTH _DISCOVERED_TOOLCHAINS_RUSTC_PATH _toolchain_rustc_len)
    list(LENGTH _DISCOVERED_TOOLCHAINS_CARGO_PATH _toolchain_cargo_len)
    list(LENGTH _DISCOVERED_TOOLCHAINS_VERSION _toolchain_version_len)
    if(NOT
        (_toolchain_len EQUAL _toolchain_rustc_len
            AND _toolchain_cargo_len EQUAL _toolchain_version_len
            AND _toolchain_len EQUAL _toolchain_cargo_len)
        )
        message(FATAL_ERROR "Internal error - list length mismatch."
            "List lengths: ${_toolchain_len} toolchains, ${_toolchain_rustc_len} rustc, ${_toolchain_cargo_len} cargo,"
            " ${_toolchain_version_len} version. The lengths should be the same."
        )
    endif()

    set(Rust_RUSTUP_TOOLCHAINS CACHE INTERNAL "List of available Rustup toolchains" "${_DISCOVERED_TOOLCHAINS}")
    set(Rust_RUSTUP_TOOLCHAINS_RUSTC_PATH
        CACHE INTERNAL
        "List of the rustc paths corresponding to the toolchain at the same index in `Rust_RUSTUP_TOOLCHAINS`."
        "${_DISCOVERED_TOOLCHAINS_RUSTC_PATH}"
    )
    set(Rust_RUSTUP_TOOLCHAINS_CARGO_PATH
        CACHE INTERNAL
        "List of the cargo paths corresponding to the toolchain at the same index in `Rust_RUSTUP_TOOLCHAINS`. \
        May also be `NOTFOUND` if the toolchain does not have a cargo executable."
        "${_DISCOVERED_TOOLCHAINS_CARGO_PATH}"
    )
    set(Rust_RUSTUP_TOOLCHAINS_VERSION
        CACHE INTERNAL
        "List of the rust toolchain version corresponding to the toolchain at the same index in \
        `Rust_RUSTUP_TOOLCHAINS`."
        "${_DISCOVERED_TOOLCHAINS_VERSION}"
    )

    # Rust_TOOLCHAIN is preferred over a requested version if it is set.
    if (NOT DEFINED Rust_TOOLCHAIN)
        if (NOT DEFINED _TOOLCHAIN_OVERRIDE)
            set(_TOOLCHAIN_SELECTED "${_TOOLCHAIN_DEFAULT}")
        else()
            set(_TOOLCHAIN_SELECTED "${_TOOLCHAIN_OVERRIDE}")
        endif()
        # Check default toolchain first.
        _findrust_version_ok("_TOOLCHAIN_${_TOOLCHAIN_SELECTED}_VERSION" _VERSION_OK)
        if(NOT "${_VERSION_OK}")
            foreach(_TOOLCHAIN "${_DISCOVERED_TOOLCHAINS}")
                _findrust_version_ok("_TOOLCHAIN_${_TOOLCHAIN}_VERSION" _VERSION_OK)
                if("${_VERSION_OK}")
                    set(_TOOLCHAIN_SELECTED "${_TOOLCHAIN}")
                    break()
                endif()
            endforeach()
            # Check if we found a suitable version in the for loop.
            if(NOT "${_VERSION_OK}")
                string(REPLACE ";" "\n" _DISCOVERED_TOOLCHAINS "${_DISCOVERED_TOOLCHAINS}")
                _findrust_failed("Failed to find a Rust toolchain matching the version requirements of "
                        "${Rust_FIND_VERSION}. Available toolchains: ${_DISCOVERED_TOOLCHAINS}")
            endif()
        endif()
    endif()

    set(Rust_TOOLCHAIN "${_TOOLCHAIN_SELECTED}" CACHE STRING "The rustup toolchain to use")
    set_property(CACHE Rust_TOOLCHAIN PROPERTY STRINGS "${_DISCOVERED_TOOLCHAINS}")

    if(NOT Rust_FIND_QUIETLY)
        message(STATUS "Rust Toolchain: ${Rust_TOOLCHAIN}")
    endif()

    if (NOT Rust_TOOLCHAIN IN_LIST _DISCOVERED_TOOLCHAINS)
        # If the precise toolchain wasn't found, try appending the default host
        execute_process(
            COMMAND
                "${Rust_RUSTUP}" show
            RESULT_VARIABLE _SHOW_RESULT
            OUTPUT_VARIABLE _SHOW_RAW
        )
        if(NOT "${_SHOW_RESULT}" EQUAL "0")
            _findrust_failed("Command `${Rust_RUSTUP} show` failed")
        endif()

        if (_SHOW_RAW MATCHES "Default host: ([a-zA-Z0-9_\\-]*)\n")
            set(_DEFAULT_HOST "${CMAKE_MATCH_1}")
        else()
            _findrust_failed("Failed to parse \"Default host\" from `${Rust_RUSTUP} show`. Got: ${_SHOW_RAW}")
        endif()

        if (NOT "${Rust_TOOLCHAIN}-${_DEFAULT_HOST}" IN_LIST _DISCOVERED_TOOLCHAINS)
            set(_NOT_FOUND_MESSAGE "Could not find toolchain '${Rust_TOOLCHAIN}'\n"
                "Available toolchains:\n"
            )
            foreach(_TOOLCHAIN ${_DISCOVERED_TOOLCHAINS})
                list(APPEND _NOT_FOUND_MESSAGE "  `${_TOOLCHAIN}`\n")
            endforeach()
            _findrust_failed(${_NOT_FOUND_MESSAGE})
        endif()

        set(_RUSTUP_TOOLCHAIN_FULL "${Rust_TOOLCHAIN}-${_DEFAULT_HOST}")
    else()
        set(_RUSTUP_TOOLCHAIN_FULL "${Rust_TOOLCHAIN}")
    endif()

    set(_RUST_TOOLCHAIN_PATH "${_TOOLCHAIN_${_RUSTUP_TOOLCHAIN_FULL}_PATH}")
    if(NOT "${Rust_FIND_QUIETLY}")
        message(VERBOSE "Rust toolchain ${_RUSTUP_TOOLCHAIN_FULL}")
        message(VERBOSE "Rust toolchain path ${_RUST_TOOLCHAIN_PATH}")
    endif()

    # Is overridden if the user specifies `Rust_COMPILER` explicitly.
    find_program(
        Rust_COMPILER_CACHED
        rustc
            HINTS "${_RUST_TOOLCHAIN_PATH}/bin"
            NO_DEFAULT_PATH)
elseif (Rust_RUSTUP)
    get_filename_component(_RUST_TOOLCHAIN_PATH "${Rust_RUSTUP}" DIRECTORY)
    get_filename_component(_RUST_TOOLCHAIN_PATH "${_RUST_TOOLCHAIN_PATH}" DIRECTORY)
    find_program(
        Rust_COMPILER_CACHED
        rustc
            HINTS "${_RUST_TOOLCHAIN_PATH}/bin"
            NO_DEFAULT_PATH)
else()
    find_program(Rust_COMPILER_CACHED rustc)
    if (EXISTS "${Rust_COMPILER_CACHED}")
        # rustc is expected to be at `<toolchain_path>/bin/rustc`.
        get_filename_component(_RUST_TOOLCHAIN_PATH "${Rust_COMPILER_CACHED}" DIRECTORY)
        get_filename_component(_RUST_TOOLCHAIN_PATH "${_RUST_TOOLCHAIN_PATH}" DIRECTORY)
    endif()
endif()

if (NOT EXISTS "${Rust_COMPILER_CACHED}")
    set(_NOT_FOUND_MESSAGE "The rustc executable was not found. "
        "Rust not installed or ~/.cargo/bin not added to path?\n"
        "Hint: Consider setting `Rust_COMPILER` to the absolute path of `rustc`."
    )
    _findrust_failed(${_NOT_FOUND_MESSAGE})
endif()

if (Rust_RESOLVE_RUSTUP_TOOLCHAINS)
    set(_NOT_FOUND_MESSAGE "Rust was detected to be managed by rustup, but failed to find `cargo` "
        "next to `rustc` in `${_RUST_TOOLCHAIN_PATH}/bin`. This can happen for custom toolchains, "
        "if cargo was not built. "
        "Please manually specify the path to a compatible `cargo` by setting `Rust_CARGO`."
    )
    find_program(
        Rust_CARGO_CACHED
        cargo
            HINTS "${_RUST_TOOLCHAIN_PATH}/bin"
            NO_DEFAULT_PATH
    )
    # note: maybe can use find_package_handle_standard_args here, if we remove the _CACHED postfix.
    # not sure why that is here...
    if(NOT EXISTS "${Rust_CARGO_CACHED}")
        _findrust_failed(${_NOT_FOUND_MESSAGE})
    endif()
    set(Rust_TOOLCHAIN_IS_RUSTUP_MANAGED TRUE CACHE INTERNAL "" FORCE)
else()
    set(_NOT_FOUND_MESSAGE "Failed to find `cargo` in PATH and `${_RUST_TOOLCHAIN_PATH}/bin`.\n"
        "Please ensure cargo is in PATH or manually specify the path to a compatible `cargo` by "
        "setting `Rust_CARGO`."
    )
    # On some systems (e.g. NixOS) cargo is not managed by rustup and also not next to rustc.
    find_program(
            Rust_CARGO_CACHED
            cargo
                HINTS "${_RUST_TOOLCHAIN_PATH}/bin"
    )
    # note: maybe can use find_package_handle_standard_args here, if we remove the _CACHED postfix.
    # not sure why that is here...
    if(NOT EXISTS "${Rust_CARGO_CACHED}")
        _findrust_failed(${_NOT_FOUND_MESSAGE})
    endif()
endif()

execute_process(
    COMMAND "${Rust_CARGO_CACHED}" --version --verbose
    OUTPUT_VARIABLE _CARGO_VERSION_RAW
    RESULT_VARIABLE _CARGO_VERSION_RESULT
)
# todo: check if cargo is a required component!
if(NOT ( "${_CARGO_VERSION_RESULT}" EQUAL "0" ))
    _findrust_failed("Failed to get cargo version.\n"
        "`${Rust_CARGO_CACHED} --version` failed with error: `${_CARGO_VERSION_RESULT}"
)
endif()

# todo: don't set cache variables here, but let find_package_handle_standard_args do the promotion
# later.
if (_CARGO_VERSION_RAW MATCHES "cargo ([0-9]+)\\.([0-9]+)\\.([0-9]+)")
    set(Rust_CARGO_VERSION_MAJOR "${CMAKE_MATCH_1}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION_MINOR "${CMAKE_MATCH_2}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION_PATCH "${CMAKE_MATCH_3}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION "${Rust_CARGO_VERSION_MAJOR}.${Rust_CARGO_VERSION_MINOR}.${Rust_CARGO_VERSION_PATCH}" CACHE INTERNAL "" FORCE)
# Workaround for the version strings where the `cargo ` prefix is missing.
elseif(_CARGO_VERSION_RAW MATCHES "([0-9]+)\\.([0-9]+)\\.([0-9]+)")
    set(Rust_CARGO_VERSION_MAJOR "${CMAKE_MATCH_1}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION_MINOR "${CMAKE_MATCH_2}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION_PATCH "${CMAKE_MATCH_3}" CACHE INTERNAL "" FORCE)
    set(Rust_CARGO_VERSION "${Rust_CARGO_VERSION_MAJOR}.${Rust_CARGO_VERSION_MINOR}.${Rust_CARGO_VERSION_PATCH}" CACHE INTERNAL "" FORCE)
else()
    _findrust_failed(
        "Failed to parse cargo version. `cargo --version` evaluated to (${_CARGO_VERSION_RAW}). "
        "Expected a <Major>.<Minor>.<Patch> version triple."
    )
endif()

execute_process(
    COMMAND "${Rust_COMPILER_CACHED}" --version --verbose
    OUTPUT_VARIABLE _RUSTC_VERSION_RAW
    RESULT_VARIABLE _RUSTC_VERSION_RESULT
)

if(NOT ( "${_RUSTC_VERSION_RESULT}" EQUAL "0" ))
    _findrust_failed("Failed to get rustc version.\n"
        "${Rust_COMPILER_CACHED} --version failed with error: `${_RUSTC_VERSION_RESULT}`")
endif()

if (_RUSTC_VERSION_RAW MATCHES "rustc ([0-9]+)\\.([0-9]+)\\.([0-9]+)(-nightly)?")
    set(Rust_VERSION_MAJOR "${CMAKE_MATCH_1}" CACHE INTERNAL "" FORCE)
    set(Rust_VERSION_MINOR "${CMAKE_MATCH_2}" CACHE INTERNAL "" FORCE)
    set(Rust_VERSION_PATCH "${CMAKE_MATCH_3}" CACHE INTERNAL "" FORCE)
    set(Rust_VERSION "${Rust_VERSION_MAJOR}.${Rust_VERSION_MINOR}.${Rust_VERSION_PATCH}" CACHE INTERNAL "" FORCE)
    if(CMAKE_MATCH_4)
        set(Rust_IS_NIGHTLY 1 CACHE INTERNAL "" FORCE)
    else()
        set(Rust_IS_NIGHTLY 0 CACHE INTERNAL "" FORCE)
    endif()
else()
    _findrust_failed("Failed to parse rustc version. `${Rust_COMPILER_CACHED} --version --verbose` "
        "evaluated to:\n`${_RUSTC_VERSION_RAW}`"
    )
endif()

if (_RUSTC_VERSION_RAW MATCHES "host: ([a-zA-Z0-9_\\-]*)\n")
    set(Rust_DEFAULT_HOST_TARGET "${CMAKE_MATCH_1}")
    set(Rust_CARGO_HOST_TARGET_CACHED "${Rust_DEFAULT_HOST_TARGET}" CACHE STRING "Host triple")
else()
    _findrust_failed(
        "Failed to parse rustc host target. `rustc --version --verbose` evaluated to:\n${_RUSTC_VERSION_RAW}"
    )
endif()

if (_RUSTC_VERSION_RAW MATCHES "LLVM version: ([0-9]+)\\.([0-9]+)(\\.([0-9]+))?")
    set(Rust_LLVM_VERSION_MAJOR "${CMAKE_MATCH_1}" CACHE INTERNAL "" FORCE)
    set(Rust_LLVM_VERSION_MINOR "${CMAKE_MATCH_2}" CACHE INTERNAL "" FORCE)
    # With the Rust toolchain 1.44.1 the reported LLVM version is 9.0, i.e. without a patch version.
    # Since cmake regex does not support non-capturing groups, just ignore Match 3.
    set(Rust_LLVM_VERSION_PATCH "${CMAKE_MATCH_4}" CACHE INTERNAL "" FORCE)
    set(Rust_LLVM_VERSION "${Rust_LLVM_VERSION_MAJOR}.${Rust_LLVM_VERSION_MINOR}.${Rust_LLVM_VERSION_PATCH}" CACHE INTERNAL "" FORCE)
elseif(NOT Rust_FIND_QUIETLY)
    message(
            WARNING
            "Failed to parse rustc LLVM version. `rustc --version --verbose` evaluated to:\n${_RUSTC_VERSION_RAW}"
    )
endif()

if (NOT Rust_CARGO_TARGET_CACHED)
    unset(_CARGO_ARCH)
    unset(_CARGO_ABI)
    if (WIN32)
        if (CMAKE_VS_PLATFORM_NAME)
            string(TOLOWER "${CMAKE_VS_PLATFORM_NAME}" LOWER_VS_PLATFORM_NAME)
            if ("${LOWER_VS_PLATFORM_NAME}" STREQUAL "win32")
                set(_CARGO_ARCH i686)
            elseif("${LOWER_VS_PLATFORM_NAME}" STREQUAL "x64")
                set(_CARGO_ARCH x86_64)
            elseif("${LOWER_VS_PLATFORM_NAME}" STREQUAL "arm64")
                set(_CARGO_ARCH aarch64)
            else()
                message(WARNING "VS Platform '${CMAKE_VS_PLATFORM_NAME}' not recognized")
            endif()
        endif()
        # Fallback path
        if(NOT DEFINED _CARGO_ARCH)
            # Possible values for windows when not cross-compiling taken from here:
            # https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details
            # When cross-compiling the user is expected to supply the value, so we match more variants.
            if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64)$")
                set(_CARGO_ARCH x86_64)
            elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$")
                set(_CARGO_ARCH aarch64)
            elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(X86|x86|i686)$")
                set(_CARGO_ARCH i686)
            elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "i586")
                set(_CARGO_ARCH i586)
            elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "IA64")
                message(FATAL_ERROR "No rust target for Intel Itanium.")
            elseif(NOT "${CMAKE_SYSTEM_PROCESSOR}")
                message(WARNING "Failed to detect target architecture. Please set `CMAKE_SYSTEM_PROCESSOR`"
                    " to your target architecture or set `Rust_CARGO_TARGET` to your cargo target triple."
                )
            else()
                message(WARNING "Failed to detect target architecture. Please set "
                    "`Rust_CARGO_TARGET` to your cargo target triple."
                )
            endif()
        endif()

        set(_CARGO_VENDOR "pc-windows")

        # The MSVC Generators will always target the msvc ABI.
        # For other generators we check the compiler ID and compiler target (if present)
        # If no compiler is set and we are not cross-compiling then we just choose the
        # default rust host target.
        if(DEFINED MSVC
            OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC"
            OR "${CMAKE_C_COMPILER_ID}" STREQUAL "MSVC"
            OR "${CMAKE_CXX_COMPILER_TARGET}" MATCHES "-msvc$"
            OR "${CMAKE_C_COMPILER_TARGET}" MATCHES "-msvc$"
        )
            set(_CARGO_ABI msvc)
        elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU"
            OR "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU"
            OR "${CMAKE_CXX_COMPILER_TARGET}" MATCHES "-gnu$"
            OR "${CMAKE_C_COMPILER_TARGET}" MATCHES "-gnu$"
            OR (NOT CMAKE_CROSSCOMPILING AND "${Rust_DEFAULT_HOST_TARGET}" MATCHES "-gnu$")
            )
            set(_CARGO_ABI gnu)
        elseif(NOT "${CMAKE_CROSSCOMPILING}" AND "${Rust_DEFAULT_HOST_TARGET}" MATCHES "-msvc$")
            # We first check if the gnu branch matches to ensure this fallback is only used
            # if no compiler is enabled.
            set(_CARGO_ABI msvc)
        else()
            message(WARNING "Could not determine the target ABI. Please specify `Rust_CARGO_TARGET` manually.")
        endif()

        if(DEFINED _CARGO_ARCH AND DEFINED _CARGO_VENDOR AND DEFINED _CARGO_ABI)
            set(Rust_CARGO_TARGET_CACHED "${_CARGO_ARCH}-${_CARGO_VENDOR}-${_CARGO_ABI}"
                CACHE STRING "Target triple")
        endif()
    elseif (ANDROID)
        if (CMAKE_ANDROID_ARCH_ABI STREQUAL armeabi-v7a)
            if (CMAKE_ANDROID_ARM_MODE)
                set(_Rust_ANDROID_TARGET armv7-linux-androideabi)
            else ()
                set(_Rust_ANDROID_TARGET thumbv7neon-linux-androideabi)
            endif()
        elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL arm64-v8a)
            set(_Rust_ANDROID_TARGET aarch64-linux-android)
        elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL x86)
            set(_Rust_ANDROID_TARGET i686-linux-android)
        elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL x86_64)
            set(_Rust_ANDROID_TARGET x86_64-linux-android)
        endif()

        if (_Rust_ANDROID_TARGET)
            set(Rust_CARGO_TARGET_CACHED "${_Rust_ANDROID_TARGET}" CACHE STRING "Target triple")
        endif()
    elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "OHOS")
        if(CMAKE_OHOS_ARCH_ABI STREQUAL arm64-v8a)
            set(_RUST_OHOS_TARGET aarch64-unknown-linux-ohos)
        elseif(CMAKE_OHOS_ARCH_ABI STREQUAL armeabi-v7a)
            set(_RUST_OHOS_TARGET armv7-unknown-linux-ohos)
        elseif(CMAKE_OHOS_ARCH_ABI STREQUAL x86_64)
            set(_RUST_OHOS_TARGET x86_64-unknown-linux-ohos)
        else()
            message(WARNING "unrecognized OHOS architecture: ${OHOS_ARCH}")
        endif()
        if(_RUST_OHOS_TARGET)
            set(Rust_CARGO_TARGET_CACHED "${_RUST_OHOS_TARGET}" CACHE STRING "Target triple")
        endif()
    endif()
    # Fallback to the default host target
    if(NOT Rust_CARGO_TARGET_CACHED)
        if(CMAKE_CROSSCOMPILING)
            message(WARNING "CMake is in cross-compiling mode, but the cargo target-triple could not be inferred."
                "Falling back to the default host target. Please consider manually setting `Rust_CARGO_TARGET`."
            )
        endif()
        set(Rust_CARGO_TARGET_CACHED "${Rust_DEFAULT_HOST_TARGET}" CACHE STRING "Target triple")
    endif()

    message(STATUS "Rust Target: ${Rust_CARGO_TARGET_CACHED}")
endif()

if(Rust_CARGO_TARGET_CACHED STREQUAL Rust_DEFAULT_HOST_TARGET)
    set(Rust_CROSSCOMPILING FALSE CACHE INTERNAL "Rust is configured for cross-compiling")
else()
    set(Rust_CROSSCOMPILING TRUE  CACHE INTERNAL "Rust is configured for cross-compiling")
endif()

_corrosion_parse_target_triple("${Rust_CARGO_TARGET_CACHED}" rust_arch rust_vendor rust_os rust_env)
_corrosion_parse_target_triple("${Rust_CARGO_HOST_TARGET_CACHED}" rust_host_arch rust_host_vendor rust_host_os rust_host_env)

set(Rust_CARGO_TARGET_ARCH "${rust_arch}" CACHE INTERNAL "Target architecture")
set(Rust_CARGO_TARGET_VENDOR "${rust_vendor}" CACHE INTERNAL "Target vendor")
set(Rust_CARGO_TARGET_OS "${rust_os}" CACHE INTERNAL "Target Operating System")
set(Rust_CARGO_TARGET_ENV "${rust_env}" CACHE INTERNAL "Target environment")

set(Rust_CARGO_HOST_ARCH "${rust_host_arch}" CACHE INTERNAL "Host architecture")
set(Rust_CARGO_HOST_VENDOR "${rust_host_vendor}" CACHE INTERNAL "Host vendor")
set(Rust_CARGO_HOST_OS "${rust_host_os}" CACHE INTERNAL "Host Operating System")
set(Rust_CARGO_HOST_ENV "${rust_host_env}" CACHE INTERNAL "Host environment")

if(NOT DEFINED CACHE{Rust_CARGO_TARGET_LINK_NATIVE_LIBS})
    message(STATUS "Determining required link libraries for target ${Rust_CARGO_TARGET_CACHED}")
    unset(required_native_libs)
    _corrosion_determine_libs_new("${Rust_CARGO_TARGET_CACHED}" required_native_libs required_link_flags)
    if(DEFINED required_native_libs)
        message(STATUS "Required static libs for target ${Rust_CARGO_TARGET_CACHED}: ${required_native_libs}" )
    endif()
    if(DEFINED required_link_flags)
        message(STATUS "Required link flags for target ${Rust_CARGO_TARGET_CACHED}: ${required_link_flags}" )
    endif()
    # In very recent corrosion versions it is possible to override the rust compiler version
    # per target, so to be totally correct we would need to determine the libraries for
    # every installed Rust version, that the user could choose from.
    # In practice there aren't likely going to be any major differences, so we just do it once
    # for the target and once for the host target (if cross-compiling).
    set(Rust_CARGO_TARGET_LINK_NATIVE_LIBS "${required_native_libs}" CACHE INTERNAL
            "Required native libraries when linking Rust static libraries")
    set(Rust_CARGO_TARGET_LINK_OPTIONS "${required_link_flags}" CACHE INTERNAL
            "Required link flags when linking Rust static libraries")
endif()

if(Rust_CROSSCOMPILING AND NOT DEFINED CACHE{Rust_CARGO_HOST_TARGET_LINK_NATIVE_LIBS})
    message(STATUS "Determining required link libraries for target ${Rust_CARGO_HOST_TARGET_CACHED}")
    unset(host_libs)
    _corrosion_determine_libs_new("${Rust_CARGO_HOST_TARGET_CACHED}" host_libs host_flags)
    if(DEFINED host_libs)
        message(STATUS "Required static libs for host target ${Rust_CARGO_HOST_TARGET_CACHED}: ${host_libs}" )
    endif()
    set(Rust_CARGO_HOST_TARGET_LINK_NATIVE_LIBS "${host_libs}" CACHE INTERNAL
        "Required native libraries when linking Rust static libraries for the host target")
    set(Rust_CARGO_HOST_TARGET_LINK_OPTIONS "${host_flags}" CACHE INTERNAL
        "Required linker flags when linking Rust static libraries for the host target")
endif()

# Set the input variables as non-cache variables so that the variables are available after
# `find_package`, even if the values were evaluated to defaults.
foreach(_VAR ${_Rust_USER_VARS})
    set(${_VAR} "${${_VAR}_CACHED}")
    # Ensure cached variables have type INTERNAL
    set(${_VAR}_CACHED "${${_VAR}_CACHED}" CACHE INTERNAL "Internal cache of ${_VAR}")
endforeach()

find_package_handle_standard_args(
    Rust
    REQUIRED_VARS Rust_COMPILER Rust_VERSION Rust_CARGO Rust_CARGO_VERSION Rust_CARGO_TARGET Rust_CARGO_HOST_TARGET
    VERSION_VAR Rust_VERSION
)


if(NOT TARGET Rust::Rustc)
    add_executable(Rust::Rustc IMPORTED GLOBAL)
    set_property(
        TARGET Rust::Rustc
        PROPERTY IMPORTED_LOCATION "${Rust_COMPILER_CACHED}"
    )

    add_executable(Rust::Cargo IMPORTED GLOBAL)
    set_property(
        TARGET Rust::Cargo
        PROPERTY IMPORTED_LOCATION "${Rust_CARGO_CACHED}"
    )
    set(Rust_FOUND true)
endif()

list(POP_BACK CMAKE_MESSAGE_CONTEXT)