From 973f63be0017d01024e6b4a950db4e6e85d8faea Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Fri, 16 Feb 2024 09:20:22 -0800 Subject: [PATCH] RustEnvironmentCheck: Check Version if specified (#737) ## Description Allows repository owners to enforce version requirements for some rust related tools needed for build. This includes cargo-make and cargo-tarpaulin. RustEnvironmentCheck currently only checks that the necessary tooling exists on the system. The only version check it performs is for the compiler version. This update adds support for repository owners to optionally require specific versions for cargo-make and cargo-tarpaulin by setting the required version in the rust-toolchain.toml file at the workspace root. Updates the install command suggestion for cargo make and cargo tarpaulin. Example Usage: ``` # rust-toolchain.toml [toolchain] channel = "1.73.0" [tool] cargo-tarpaulin = "0.27.3" cargo-make = "0.37.9" ``` - [ ] 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 Confirmed the following scenarios: 1. Tool missing, version specified ![image](https://github.com/microsoft/mu_basecore/assets/24388509/3664dca1-b1e2-42f7-bb6c-f5634cd87ffa) 3. Tool missing, version not specified ![image](https://github.com/microsoft/mu_basecore/assets/24388509/807754fa-e537-4b54-a670-1c1c4a908a33) 5. Tool installed, version mismatch ![image](https://github.com/microsoft/mu_basecore/assets/24388509/db0131d9-f8da-4ef4-910a-c8565175a168) ## Integration Instructions If a repository maintainer wants to manage the version, update the rust-toolchain.toml file similar to the example below: Example Usage: ``` [toolchain] channel = "1.73.0" [tool] cargo-tarpaulin = "0.27.3" cargo-make = "0.37.9" ``` --- .../RustEnvironmentCheck.py | 125 ++++++++++++++---- 1 file changed, 96 insertions(+), 29 deletions(-) diff --git a/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py index 989d95450c..bd348b9b46 100644 --- a/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py +++ b/BaseTools/Plugin/RustEnvironmentCheck/RustEnvironmentCheck.py @@ -29,10 +29,9 @@ WORKSPACE_TOOLCHAIN_FILE = "rust-toolchain.toml" -RustToolInfo = namedtuple("RustToolInfo", ["presence_cmd", "install_help"]) +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.""" @@ -46,20 +45,37 @@ def do_pre_build(self, _: UefiBuilder) -> int: int: The number of environment issues found. Zero indicates no action is needed. """ - def verify_cmd(name: str, params: str = "--version") -> bool: + def verify_cmd(tool: RustToolInfo) -> int: """Indicates if a command can successfully be executed. Args: - name (str): Tool name. - params (str, optional): Tool params. Defaults to "--version". + tool (RustToolInfo): Tool information Returns: - bool: True on success. False on failure to run the command. + int: 0 for success, 1 for missing tool, 2 for version mismatch """ 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) - return ret == 0 + + if ret != 0: + 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 @@ -81,6 +97,27 @@ def get_workspace_toolchain_version() -> RustToolChainInfo: # 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) + 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. @@ -170,47 +207,68 @@ def verify_rust_src_component_is_installed() -> bool: 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 + install_help=generic_rust_install_instructions, + required_version=None, + regex=None, ), "rustc": RustToolInfo( presence_cmd=("rustc",), - install_help=generic_rust_install_instructions + install_help=generic_rust_install_instructions, + required_version=None, + regex=None, ), "cargo": RustToolInfo( presence_cmd=("cargo",), - install_help=generic_rust_install_instructions + 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 + 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 + 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 + 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 + install_help=generic_rust_install_instructions, + required_version=None, + regex=None, ), "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." + 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="View the installation instructions at " - "https://crates.io/crates/cargo-tarpaulin to install Cargo " - "tarpaulin. A tool used for Rust code coverage." + 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+' ), } @@ -222,14 +280,23 @@ def verify_rust_src_component_is_installed() -> bool: errors = 0 for tool_name, tool_info in tools.items(): - if tool_name not in excluded_tools and 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 + 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 rust_toolchain_info = verify_workspace_rust_toolchain_is_installed() if rust_toolchain_info.error: