# @file CodeQlBuildPlugin.py # # A build plugin that produces CodeQL results for the present build. # # Copyright (c) Microsoft Corporation. All rights reserved. # SPDX-License-Identifier: BSD-2-Clause-Patent ## import glob import logging import os import stat from common import codeql_plugin from pathlib import Path from edk2toolext import edk2_logging from edk2toolext.environment.plugintypes.uefi_build_plugin import \ IUefiBuildPlugin from edk2toolext.environment.uefi_build import UefiBuilder from edk2toollib.uefi.edk2.path_utilities import Edk2Path from edk2toollib.utility_functions import GetHostInfo, RemoveTree class CodeQlBuildPlugin(IUefiBuildPlugin): def do_pre_build(self, builder: UefiBuilder) -> int: """CodeQL pre-build functionality. Args: builder (UefiBuilder): A UEFI builder object for this build. Returns: int: The plugin return code. Zero indicates the plugin ran successfully. A non-zero value indicates an unexpected error occurred during plugin execution. """ if not builder.SkipBuild: self.builder = builder self.package = builder.edk2path.GetContainingPackage( builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath( builder.env.GetValue("ACTIVE_PLATFORM") ) ) self.target = builder.env.GetValue("TARGET") self.build_output_dir = builder.env.GetValue("BUILD_OUTPUT_BASE") self.codeql_db_path = codeql_plugin.get_codeql_db_path( builder.ws, self.package, self.target) edk2_logging.log_progress(f"{self.package} will be built for CodeQL") edk2_logging.log_progress(f" CodeQL database will be written to " f"{self.codeql_db_path}") self.codeql_path = codeql_plugin.get_codeql_cli_path() if not self.codeql_path: logging.critical("CodeQL build enabled but CodeQL CLI application " "not found.") return -1 # CodeQL can only generate a database on clean build # # Note: builder.CleanTree() cannot be used here as some platforms # have build steps that run before this plugin that store # files in the build output directory. # # CodeQL does not care about with those files or many others such # as the FV directory, build logs, etc. so instead focus on # removing only the directories with compilation/linker output # for the architectures being built (that need clean runs for # CodeQL to work). targets = self.builder.env.GetValue("TARGET_ARCH").split(" ") for target in targets: directory_to_delete = Path(self.build_output_dir, target) if directory_to_delete.is_dir(): logging.debug(f"Removing {str(directory_to_delete)} to have a " f"clean build for CodeQL.") RemoveTree(str(directory_to_delete)) # CodeQL CLI does not handle spaces passed in CLI commands well # (perhaps at all) as discussed here: # 1. https://github.com/github/codeql-cli-binaries/issues/73 # 2. https://github.com/github/codeql/issues/4910 # # Since it's unclear how quotes are handled and may change in the # future, this code is going to use the workaround to place the # command in an executable file that is instead passed to CodeQL. self.codeql_cmd_path = Path(self.build_output_dir, "codeql_build_command") build_params = self._get_build_params() codeql_build_cmd = "" if GetHostInfo().os == "Windows": self.codeql_cmd_path = self.codeql_cmd_path.parent / ( self.codeql_cmd_path.name + '.bat') elif GetHostInfo().os == "Linux": self.codeql_cmd_path = self.codeql_cmd_path.parent / ( self.codeql_cmd_path.name + '.sh') codeql_build_cmd += f"#!/bin/bash{os.linesep * 2}" codeql_build_cmd += "build " + build_params self.codeql_cmd_path.parent.mkdir(exist_ok=True, parents=True) self.codeql_cmd_path.write_text(encoding='utf8', data=codeql_build_cmd) if GetHostInfo().os == "Linux": os.chmod(self.codeql_cmd_path, os.stat(self.codeql_cmd_path).st_mode | stat.S_IEXEC) for f in glob.glob(os.path.join( os.path.dirname(self.codeql_path), '**/*'), recursive=True): os.chmod(f, os.stat(f).st_mode | stat.S_IEXEC) codeql_params = (f'database create {self.codeql_db_path} ' f'--language=cpp ' f'--source-root={builder.ws} ' f'--command={self.codeql_cmd_path}') # Set environment variables so the CodeQL build command is picked up # as the active build command. # # Note: Requires recent changes in edk2-pytool-extensions (0.20.0) # to support reading these variables. builder.env.SetValue( "EDK_BUILD_CMD", self.codeql_path, "Set in CodeQL Build Plugin") builder.env.SetValue( "EDK_BUILD_PARAMS", codeql_params, "Set in CodeQL Build Plugin") return 0 def _get_build_params(self) -> str: """Returns the build command parameters for this build. Based on the well-defined `build` command-line parameters. Returns: str: A string representing the parameters for the build command. """ build_params = f"-p {self.builder.env.GetValue('ACTIVE_PLATFORM')}" build_params += f" -b {self.target}" build_params += f" -t {self.builder.env.GetValue('TOOL_CHAIN_TAG')}" max_threads = self.builder.env.GetValue('MAX_CONCURRENT_THREAD_NUMBER') if max_threads is not None: build_params += f" -n {max_threads}" rt = self.builder.env.GetValue("TARGET_ARCH").split(" ") for t in rt: build_params += " -a " + t if (self.builder.env.GetValue("BUILDREPORTING") == "TRUE"): build_params += (" -y " + self.builder.env.GetValue("BUILDREPORT_FILE")) rt = self.builder.env.GetValue("BUILDREPORT_TYPES").split(" ") for t in rt: build_params += " -Y " + t # add special processing to handle building a single module mod = self.builder.env.GetValue("BUILDMODULE") if (mod is not None and len(mod.strip()) > 0): build_params += " -m " + mod edk2_logging.log_progress("Single Module Build: " + mod) build_vars = self.builder.env.GetAllBuildKeyValues(self.target) for key, value in build_vars.items(): build_params += " -D " + key + "=" + value return build_params