Skip to content

Commit

Permalink
BaseTools/Plugin: Add Rust Environment Check build plugin (#600)
Browse files Browse the repository at this point in the history
## Description

Firmware developer's machines are often not setup for Rust. Ad more
Rust code is proliferating across the repos, this plugin is used to
provide early and direct feedback about the developer's environment
so it can successfully build Rust code using the tools commonly
used in the firmware build process.

The plugin is run when the `rust-ci` scope is specified which is used
by platforms to opt into Rust plugin support in Project Mu.

The entire plugin takes ~1.4 sec to run on average so build time is
not meaningfully impacted.

- [x] Impacts functionality?
- **Functionality** - Does the change ultimately impact how firmware
functions?
- Examples: Add a new library, publish a new PPI, update an algorithm,
...
- [ ] Impacts security?
- **Security** - Does the change have a direct security impact on an
application, flow, or firmware?
  - Examples: Crypto algorithm change, buffer overflow fix, parameter
    validation improvement, ...
- [ ] Breaking change?
- **Breaking change** - Will anyone consuming this change experience a
break in build or boot behavior?
- Examples: Add a new library class, move a module to a different repo,
call a function in a new library class in a pre-existing module, ...
- [ ] Includes tests?
  - **Tests** - Does the change include any explicit test code?
  - Examples: Unit tests, integration tests, robot tests, ...
- [ ] Includes documentation?
- **Documentation** - Does the change contain explicit documentation
additions outside direct code modifications (and comments)?
- Examples: Update readme file, add feature readme file, link to
documentation on an a separate Web page, ...

## How This Was Tested

Verified:

- The plugin does not run if the `rust-ci` scope is not set
- The plugin does run if the `rust-ci` scope is set
- The plugin reports no errors if all expected tools are installed.
- The plugin reports the corresponding tool installation help text if a
  given tools is not installed.
- The plugin can properly parse a `rust-toolchain.toml` file
- The plugin reports errors in the console and build log as expected.

## Integration Instructions

Enable the `rust-ci` scope if Rust code is being built. The plugin will
run when that scope is set.

Signed-off-by: Michael Kubacki <[email protected]>
  • Loading branch information
makubacki authored Oct 25, 2023
1 parent 461a59c commit 963f6bf
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 0 deletions.
173 changes: 173 additions & 0 deletions BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py
Original file line number Diff line number Diff line change
@@ -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}-"
"<host>\"\n\n"
" \"rustup component add rust-src "
f"{rust_toolchain_info.toolchain}-<host>\"")
errors += 1

return errors
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 963f6bf

Please sign in to comment.