Skip to content

Commit

Permalink
BaseTools/Plugin/RustEnvironmentCheck: Use pytools Rust helpers (#1037)
Browse files Browse the repository at this point in the history
## Description

The plugin implementation has moved to edk2-pytool-extensions so it
can be reused for plugins targeting different scenarios such as
public/generic (this plugin) or custom internal environments that
may need to add on additional functionality.

This simplifies this plugin's implementation significantly.

- [ ] Impacts functionality?
- [ ] Impacts security?
- [ ] Breaking change?
- [ ] Includes tests?
- [ ] Includes documentation?

## How This Was Tested

- Verified plugin still detects errors properly
- Unit tests added in edk2-pytool-extensions

## Integration Instructions

An `id` has been added to the plugin YAML file (`rust-env-check`). This
retains the same scope as before (`rust-ci`) but allows a custom version
of the plugin to override this version by specifying:

- `"id_override": "rust-env-check"`

In its YAML file. Otherwise, no integration work is needed.

> There is an example of code that sets `id_override` (via generated
YAML)
[here](https://github.com/microsoft/mu_basecore/blob/7cf3382ebaf5b5b5a2487d7d13e910c1c7dac233/BaseTools/Edk2ToolsBuild.py#L106-L107)
for reference.

Signed-off-by: Michael Kubacki <[email protected]>
  • Loading branch information
makubacki authored Jul 12, 2024
1 parent 7cf3382 commit 90adf2b
Show file tree
Hide file tree
Showing 3 changed files with 7 additions and 322 deletions.
324 changes: 4 additions & 320 deletions BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,16 @@
# fail much later during firmware code compilation when Rust tools are invoked
# with messages that are ambiguous or difficult to find.
#
# Note:
# - The entire plugin is enabled/disabled by scope.
# - Individual tools can be opted out by setting the environment variable
# `RUST_ENV_CHECK_TOOL_EXCLUSIONS` with a comma separated list of the tools
# to exclude. For example, "rustup, cargo tarpaulin" would not require that
# those tools be installed.
# See the following documentation for more details:
# https://www.tianocore.org/edk2-pytool-extensions/features/rust_environment/
#
# Copyright (c) Microsoft Corporation.
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import logging
import re
from collections import namedtuple
from edk2toolext.environment import shell_environment
from edk2toolext.environment.rust import run
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

WORKSPACE_TOOLCHAIN_FILE = "rust-toolchain.toml"

RustToolInfo = namedtuple("RustToolInfo", ["presence_cmd", "install_help", "required_version", "regex"])
RustToolChainInfo = namedtuple("RustToolChainInfo", ["error", "toolchain"])

class RustEnvironmentCheck(IUefiBuildPlugin):
"""Checks that the system environment is ready to build Rust code."""
Expand All @@ -45,306 +31,4 @@ def do_pre_build(self, _: UefiBuilder) -> int:
int: The number of environment issues found. Zero indicates no
action is needed.
"""
def is_corrupted_component(tool: RustToolInfo, cmd_output: str) -> bool:
"""Checks if a component should be removed and reinstalled.
Args:
tool (RustToolInfo): Tool information.
cmd_output (str): The output from a command that will be
inspected by this function.
Returns:
bool: True if the component should be removed and added back to
correct its installation.
"""
return (
f"error: the '{tool.presence_cmd[0]}' binary, normally "
f"provided by the '{tool.presence_cmd[0]}' component, is "
f"not applicable to the ") in cmd_output

def verify_cmd(tool: RustToolInfo) -> int:
"""Indicates if a command can successfully be executed.
Args:
tool (RustToolInfo): Tool information
Returns:
int: 0 for success, 1 for missing tool, 2 for version mismatch,
3 if a component is present but broken
"""
cmd_output = StringIO()
params = "--version"
name = tool.presence_cmd[0]
if len(tool.presence_cmd) == 2:
params = tool.presence_cmd[1]
ret = RunCmd(name, params, outstream=cmd_output,
logging_level=logging.DEBUG)

if ret != 0:
if is_corrupted_component(tool, cmd_output.getvalue()):
return 3
return 1

# If a specific version is required, check the version, returning
# false if there is a version mismatch
if tool.required_version:
match = re.search(tool.regex, cmd_output.getvalue())
if match is None:
logging.warning(f"Failed to verify version: {tool.required_version}")
return 0
if match.group(0) != tool.required_version:
return 2

return 0

def get_workspace_toolchain_version() -> RustToolChainInfo:
"""Returns the rust toolchain version specified in the workspace
toolchain file.
Returns:
RustToolChainInfo: The rust toolchain information. If an error
occurs, the error field will be True with no toolchain info.
"""
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)
return RustToolChainInfo(error=False, toolchain=toolchain_version)
except FileNotFoundError:
# If a file is not found. Do not check any further.
return RustToolChainInfo(error=True, toolchain=None)

def get_required_tool_versions() -> dict[str,str]:
"""Returns any tools and their required versions from the workspace
toolchain file.
Returns:
dict[str,str]: dict where the key is the tool name and the
value is the version
"""
tool_versions = {}
try:
with open(WORKSPACE_TOOLCHAIN_FILE, 'r') as toml_file:
content = toml_file.read()
match = re.search(r'\[tool\]\n((?:.+\s*=\s*.+\n)*)', content)
if match:
for line in match.group(1).splitlines():
(tool, version) = line.split('=',maxsplit=1)
tool_versions[tool.strip()] = version.strip(" \"'")
return tool_versions
except FileNotFoundError:
# If a file is not found. Do not check any further.
return tool_versions

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.
"""
toolchain_version = get_workspace_toolchain_version()
if toolchain_version.error or 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)

toolchain_version = toolchain_version.toolchain

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)

def verify_rust_src_component_is_installed() -> bool:
"""Verifies the rust-src component is installed.
Returns:
bool: True if the rust-src component is installed for the default
toolchain or the status could not be determined, otherwise, False.
"""
toolchain_version = get_workspace_toolchain_version()
if toolchain_version.error or not toolchain_version:
# If the file is not in an expected format, let that be handled
# elsewhere and do not look further.
return True

toolchain_version = toolchain_version.toolchain

rustup_output = StringIO()
ret = RunCmd("rustc", "--version --verbose",
outstream=rustup_output,
logging_level=logging.DEBUG)
if ret != 0:
# rustc installation is checked elsewhere. Exit here on failure.
return True

for line in rustup_output.getvalue().splitlines():
start_index = line.lower().strip().find("host: ")
if start_index != -1:
target_triple = line[start_index + len("host: "):]
break
else:
logging.error("Failed to get host target triple information.")
return False

rustup_output = StringIO()
ret = RunCmd("rustup", f"component list --toolchain {toolchain_version}",
outstream=rustup_output,
logging_level=logging.DEBUG)
if ret != 0:
# rustup installation and the toolchain are checked elsewhere.
# Exit here on failure.
return True

for component in rustup_output.getvalue().splitlines():
if "rust-src (installed)" in component:
return True

logging.error("The Rust toolchain is installed but the rust-src component "
"needs to be installed:\n\n"
f" rustup component add --toolchain {toolchain_version}-"
f"{target_triple} rust-src")

return False

generic_rust_install_instructions = \
"Visit https://rustup.rs/ to install Rust and cargo."
tool_versions = get_required_tool_versions()

tools = {
"rustup": RustToolInfo(
presence_cmd=("rustup",),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"rustc": RustToolInfo(
presence_cmd=("rustc",),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo": RustToolInfo(
presence_cmd=("cargo",),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo build": RustToolInfo(
presence_cmd=("cargo", "build --help"),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo check": RustToolInfo(
presence_cmd=("cargo", "check --help"),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo fmt": RustToolInfo(
presence_cmd=("cargo", "fmt --help"),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo test": RustToolInfo(
presence_cmd=("cargo", "test --help"),
install_help=generic_rust_install_instructions,
required_version=None,
regex=None,
),
"cargo make": RustToolInfo(
presence_cmd=("cargo", "make --version"),
install_help= \
f" cargo binstall cargo-make {('--version ' + tool_versions.get('cargo-make', '')) if 'cargo-make' in tool_versions else ''}"
"\nOR\n"
f" cargo install cargo-make {('--version ' + tool_versions.get('cargo-make', '')) if 'cargo-make' in tool_versions else ''}\n",
required_version=tool_versions.get("cargo-make"),
regex = r'\d+\.\d+\.\d+'
),
"cargo tarpaulin": RustToolInfo(
presence_cmd=("cargo", "tarpaulin --version"),
install_help= \
f" cargo binstall cargo-tarpaulin {('--version ' + tool_versions.get('cargo-tarpaulin', '')) if 'cargo-tarpaulin' in tool_versions else ''}"
"\nOR\n"
f" cargo install cargo-tarpaulin {('--version ' + tool_versions.get('cargo-tarpaulin', '')) if 'cargo-tarpaulin' in tool_versions else ''}\n",
required_version=tool_versions.get("cargo-tarpaulin"),
regex = r'\d+\.\d+\.\d+'
),
}

excluded_tools_in_shell = shell_environment.GetEnvironment().get_shell_var(
"RUST_ENV_CHECK_TOOL_EXCLUSIONS")
excluded_tools = ([t.strip() for t in
excluded_tools_in_shell.split(",")] if
excluded_tools_in_shell else [])

errors = 0
for tool_name, tool_info in tools.items():
if tool_name not in excluded_tools:
ret = verify_cmd(tool_info)
if ret == 1:
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
if ret == 2:
logging.error(
f"Rust Environment Failure: {tool_name} version mismatch.\n\n"
f"Expected version: {tool_info.required_version}\n\n"
f"Instructions:\n{tool_info.install_help}"
)
errors += 1
if ret == 3:
logging.error(
f"Rust Environment Failure: {tool_name} is installed "
"but does not run correctly.\n\n"
f"Run \"rustup component remove {tool_name}\"\n"
f" \"rustup component add {tool_name}\"\n\n"
f"Then try 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}-"
"<host>\"\n\n"
" \"rustup component add rust-src "
f"{rust_toolchain_info.toolchain}-<host>\"")
errors += 1

if not verify_rust_src_component_is_installed():
errors += 1

return errors
return run()
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
# 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.
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
{
"scope": "rust-ci",
"id": "rust-env-check",
"name": "Rust Environment Check Pre-Build Plugin",
"module": "RustEnvironmentCheck"
}
2 changes: 1 addition & 1 deletion pip-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
##

edk2-pytool-library~=0.21.8 # MU_CHANGE
edk2-pytool-extensions~=0.27.9 # MU_CHANGE
edk2-pytool-extensions~=0.27.10 # MU_CHANGE
antlr4-python3-runtime==4.13.1
regex
lcov-cobertura==2.0.2
Expand Down

0 comments on commit 90adf2b

Please sign in to comment.