1# @file dependency_check.py
2#
3# Copyright (c) Microsoft Corporation.
4# SPDX-License-Identifier: BSD-2-Clause-Patent
5##
6
7import logging
8import os
9from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
10from edk2toollib.uefi.edk2.parsers.inf_parser import InfParser
11from edk2toolext.environment.var_dict import VarDict
12
13
14class DependencyCheck(ICiBuildPlugin):
15    """
16    A CiBuildPlugin that finds all modules (inf files) in a package and reviews the packages used
17    to confirm they are acceptable.  This is to help enforce layering and identify improper
18    dependencies between packages.
19
20    Configuration options:
21    "DependencyCheck": {
22        "AcceptableDependencies": [], # Package dec files that are allowed in all INFs.  Example: MdePkg/MdePkg.dec
23        "AcceptableDependencies-<MODULE_TYPE>": [], # OPTIONAL Package dependencies for INFs that are HOST_APPLICATION
24        "AcceptableDependencies-HOST_APPLICATION": [], # EXAMPLE Package dependencies for INFs that are HOST_APPLICATION
25        "IgnoreInf": []  # Ignore INF if found in filesystem
26    }
27    """
28
29    def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
30        """ Provide the testcase name and classname for use in reporting
31
32            Args:
33              packagename: string containing name of package to build
34              environment: The VarDict for the test to run in
35            Returns:
36                a tuple containing the testcase name and the classname
37                (testcasename, classname)
38                testclassname: a descriptive string for the testcase can include whitespace
39                classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
40        """
41        return ("Test Package Dependencies for modules in " + packagename, packagename + ".DependencyCheck")
42
43    ##
44    # External function of plugin.  This function is used to perform the task of the MuBuild Plugin
45    #
46    #   - package is the edk2 path to package.  This means workspace/packagepath relative.
47    #   - edk2path object configured with workspace and packages path
48    #   - PkgConfig Object (dict) for the pkg
49    #   - EnvConfig Object
50    #   - Plugin Manager Instance
51    #   - Plugin Helper Obj Instance
52    #   - Junit Logger
53    #   - output_stream the StringIO output stream from this plugin via logging
54    def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):
55        overall_status = 0
56
57        # Get current platform
58        abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath(packagename)
59
60        # Get INF Files
61        INFFiles = self.WalkDirectoryForExtension([".inf"], abs_pkg_path)
62        INFFiles = [Edk2pathObj.GetEdk2RelativePathFromAbsolutePath(x) for x in INFFiles]  # make edk2relative path so can compare with Ignore List
63
64        # Remove ignored INFs
65        if "IgnoreInf" in pkgconfig:
66            for a in pkgconfig["IgnoreInf"]:
67                a = a.replace(os.sep, "/")  ## convert path sep in case ignore list is bad.  Can't change case
68                try:
69                    INFFiles.remove(a)
70                    tc.LogStdOut("IgnoreInf {0}".format(a))
71                except:
72                    logging.info("DependencyConfig.IgnoreInf -> {0} not found in filesystem.  Invalid ignore file".format(a))
73                    tc.LogStdError("DependencyConfig.IgnoreInf -> {0} not found in filesystem.  Invalid ignore file".format(a))
74
75
76        # Get the AccpetableDependencies list
77        if "AcceptableDependencies" not in pkgconfig:
78            logging.info("DependencyCheck Skipped.  No Acceptable Dependencies defined.")
79            tc.LogStdOut("DependencyCheck Skipped.  No Acceptable Dependencies defined.")
80            tc.SetSkipped()
81            return -1
82
83        # Log dependencies
84        for k in pkgconfig.keys():
85            if k.startswith("AcceptableDependencies"):
86                pkgstring = "\n".join(pkgconfig[k])
87                if ("-" in k):
88                    _, _, mod_type = k.partition("-")
89                    tc.LogStdOut(f"Additional dependencies for MODULE_TYPE {mod_type}:\n {pkgstring}")
90                else:
91                    tc.LogStdOut(f"Acceptable Dependencies:\n {pkgstring}")
92
93        # For each INF file
94        for file in INFFiles:
95            ip = InfParser()
96            logging.debug("Parsing " + file)
97            ip.SetBaseAbsPath(Edk2pathObj.WorkspacePath).SetPackagePaths(Edk2pathObj.PackagePathList).ParseFile(file)
98
99            if("MODULE_TYPE" not in ip.Dict):
100                tc.LogStdOut("Ignoring INF. Missing key for MODULE_TYPE {0}".format(file))
101                continue
102
103            mod_type = ip.Dict["MODULE_TYPE"].upper()
104            for p in ip.PackagesUsed:
105                if p not in pkgconfig["AcceptableDependencies"]:
106                    # If not in the main acceptable dependencies list then check module specific
107                    mod_specific_key = "AcceptableDependencies-" + mod_type
108                    if mod_specific_key in pkgconfig and p in pkgconfig[mod_specific_key]:
109                        continue
110
111                    logging.error("Dependency Check: Invalid Dependency INF: {0} depends on pkg {1}".format(file, p))
112                    tc.LogStdError("Dependency Check: Invalid Dependency INF: {0} depends on pkg {1}".format(file, p))
113                    overall_status += 1
114
115        # If XML object exists, add results
116        if overall_status != 0:
117            tc.SetFailed("Failed with {0} errors".format(overall_status), "DEPENDENCYCHECK_FAILED")
118        else:
119            tc.SetSuccess()
120        return overall_status
121