diff --git a/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py new file mode 100644 index 00000000000..d841a20c4d5 --- /dev/null +++ b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py @@ -0,0 +1,173 @@ +# @file RustEnvironmentCheck.py +# Plugin to confirm Rust tools are present needed to compile Rust code during +# firmare build. +# +# This provides faster, direct feedback to a developer about the changes they +# may need to make to successfully build Rust code. Otherwise, the build will +# fail much later during firmware code compilation when Rust tools are invoked +# with messages that are ambiguous or difficult to find. +# +# Copyright (c) Microsoft Corporation. +# +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import logging +import re +from collections import namedtuple +from edk2toolext.environment.plugintypes.uefi_build_plugin import IUefiBuildPlugin +from edk2toolext.environment.uefi_build import UefiBuilder +from edk2toollib.utility_functions import RunCmd +from io import StringIO + +RustToolInfo = namedtuple("RustToolInfo", ["presence_cmd", "install_help"]) +RustToolChainInfo = namedtuple("RustToolChainInfo", ["error", "toolchain"]) + + +class RustEnvironmentCheck(IUefiBuildPlugin): + """Checks that the system environment is ready to build Rust code.""" + + def do_pre_build(self, _: UefiBuilder) -> int: + """Rust environment checks during pre-build. + + Args: + builder (UefiBuilder): A UEFI builder object for this build. + + Returns: + int: The number of environment issues found. Zero indicates no + action is needed. + """ + def verify_cmd(name: str, params: str = "--version") -> bool: + """Indicates if a command can successfully be executed. + + Args: + name (str): Tool name. + params (str, optional): Tool params. Defaults to "--version". + + Returns: + bool: True on success. False on failure to run the command. + """ + cmd_output = StringIO() + ret = RunCmd(name, params, outstream=cmd_output, + logging_level=logging.DEBUG) + return ret == 0 + + def verify_workspace_rust_toolchain_is_installed() -> RustToolChainInfo: + """Verifies the rust toolchain used in the workspace is available. + + Note: This function does not use the toml library to parse the toml + file since the file is very simple and its not desirable to add the + toml module as a dependency. + + Returns: + RustToolChainInfo: A tuple that indicates if the toolchain is + available and any the toolchain version if found. + """ + WORKSPACE_TOOLCHAIN_FILE = "rust-toolchain.toml" + + toolchain_version = None + try: + with open(WORKSPACE_TOOLCHAIN_FILE, 'r') as toml_file: + content = toml_file.read() + match = re.search(r'channel\s*=\s*"([^"]+)"', content) + if match: + toolchain_version = match.group(1) + except FileNotFoundError: + # If a file is not found. Do not check any further. + return RustToolChainInfo(error=False, toolchain=None) + + if not toolchain_version: + # If the file is not in an expected format, let that be handled + # elsewhere and do not look further. + return RustToolChainInfo(error=False, toolchain=None) + + installed_toolchains = StringIO() + ret = RunCmd("rustup", "toolchain list", + outstream=installed_toolchains, + logging_level=logging.DEBUG) + + # The ability to call "rustup" is checked separately. Here do not + # continue if the command is not successful. + if ret != 0: + return RustToolChainInfo(error=False, toolchain=None) + + installed_toolchains = installed_toolchains.getvalue().splitlines() + return RustToolChainInfo( + error=not any(toolchain_version in toolchain + for toolchain in installed_toolchains), + toolchain=toolchain_version) + + generic_rust_install_instructions = \ + "Visit https://rustup.rs/ to install Rust and cargo." + + tools = { + "rustup": RustToolInfo( + presence_cmd=("rustup",), + install_help=generic_rust_install_instructions + ), + "rustc": RustToolInfo( + presence_cmd=("rustc",), + install_help=generic_rust_install_instructions + ), + "cargo": RustToolInfo( + presence_cmd=("cargo",), + install_help=generic_rust_install_instructions + ), + "cargo build": RustToolInfo( + presence_cmd=("cargo", "build --help"), + install_help=generic_rust_install_instructions + ), + "cargo check": RustToolInfo( + presence_cmd=("cargo", "check --help"), + install_help=generic_rust_install_instructions + ), + "cargo fmt": RustToolInfo( + presence_cmd=("cargo", "fmt --help"), + install_help=generic_rust_install_instructions + ), + "cargo test": RustToolInfo( + presence_cmd=("cargo", "test --help"), + install_help=generic_rust_install_instructions + ), + "cargo make": RustToolInfo( + presence_cmd=("cargo", "make --version"), + install_help="Read installation instructions at " + "https://github.com/sagiegurari/cargo-make#installation " + "to install Cargo make." + ), + "cargo tarpaulin": RustToolInfo( + presence_cmd=("cargo", "tarpaulin --version"), + install_help="View the installation instructions at " + "https://crates.io/crates/cargo-tarpaulin to install Cargo " + "tarpaulin. A tool used for Rust code coverage." + ), + } + + errors = 0 + for tool_name, tool_info in tools.items(): + if not verify_cmd(*tool_info.presence_cmd): + logging.error( + f"Rust Environment Failure: {tool_name} is not installed " + "or not on the system path.\n\n" + f"Instructions:\n{tool_info.install_help}\n\n" + f"Ensure \"{' '.join(tool_info.presence_cmd)}\" can " + "successfully be run from a terminal before trying again.") + errors += 1 + + rust_toolchain_info = verify_workspace_rust_toolchain_is_installed() + if rust_toolchain_info.error: + # The "rustc -Vv" command could be run in the script with the + # output given to the user. This is approach is also meant to show + # the user how to use the tools since getting the target triple is + # important. + logging.error( + f"This workspace requires the {rust_toolchain_info.toolchain} " + "toolchain.\n\n" + "Run \"rustc -Vv\" and use the \"host\" value to install the " + "toolchain needed:\n" + f" \"rustup toolchain install {rust_toolchain_info.toolchain}-" + "\"\n\n" + " \"rustup component add rust-src " + f"{rust_toolchain_info.toolchain}-\"") + errors += 1 + + return errors diff --git a/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck_plug_in.yaml b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck_plug_in.yaml new file mode 100644 index 00000000000..b839df47b70 --- /dev/null +++ b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck_plug_in.yaml @@ -0,0 +1,16 @@ +## @file +# Build plugin used to check that the users environment is ready to build with the Rust tools +# commonly used. This provides direct feedback early in the build process rather than ambiguous +# hard to find messages later during code compilation. +# +# This plugin requires the "rust-ci" scope to be set which is used in the plugins to determine +# when a workspace is building code that includes Rust. +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +{ + "scope": "rust-ci", + "name": "Rust Environment Check Pre-Build Plugin", + "module": "RustEnvironmentCheck" +}