import itertools
import os
import platform
import re
import subprocess
import sys
import errno

import lit.util
from lit.llvm.subst import FindTool
from lit.llvm.subst import ToolSubst

lit_path_displayed = False


class LLVMConfig(object):
    def __init__(self, lit_config, config):
        self.lit_config = lit_config
        self.config = config

        features = config.available_features

        self.use_lit_shell = False
        # Tweak PATH for Win32 to decide to use bash.exe or not.
        if sys.platform == "win32":
            # Seek necessary tools in directories and set to $PATH.
            path = None
            lit_tools_dir = getattr(config, "lit_tools_dir", None)
            required_tools = ["cmp.exe", "grep.exe", "sed.exe", "diff.exe", "echo.exe"]
            path = self.lit_config.getToolsPath(
                lit_tools_dir, config.environment["PATH"], required_tools
            )
            if path is None:
                path = self._find_git_windows_unix_tools(required_tools)
            if path is not None:
                self.with_environment("PATH", path, append_path=True)
            # Many tools behave strangely if these environment variables aren't
            # set.
            self.with_system_environment(
                ["SystemDrive", "SystemRoot", "TEMP", "TMP", "PLATFORM"]
            )
            self.use_lit_shell = True

            global lit_path_displayed
            if not self.lit_config.quiet and lit_path_displayed is False:
                self.lit_config.note("using lit tools: {}".format(path))
                lit_path_displayed = True

        # Choose between lit's internal shell pipeline runner and a real shell.
        # If LIT_USE_INTERNAL_SHELL is in the environment, we use that as an
        # override.
        lit_shell_env = os.environ.get("LIT_USE_INTERNAL_SHELL")
        if lit_shell_env:
            self.use_lit_shell = lit.util.pythonize_bool(lit_shell_env)

        if not self.use_lit_shell:
            features.add("shell")

        self.with_system_environment(
            [
                "ASAN_SYMBOLIZER_PATH",
                "HWASAN_SYMBOLIZER_PATH",
                "MSAN_SYMBOLIZER_PATH",
                "TSAN_SYMBOLIZER_PATH",
                "UBSAN_SYMBOLIZER_PATH" "ASAN_OPTIONS",
                "HWASAN_OPTIONS",
                "MSAN_OPTIONS",
                "TSAN_OPTIONS",
                "UBSAN_OPTIONS",
            ]
        )

        # Running on Darwin OS
        if platform.system() == "Darwin":
            # FIXME: lld uses the first, other projects use the second.
            # We should standardize on the former.
            features.add("system-linker-mach-o")
            features.add("system-darwin")
        elif platform.system() == "Windows":
            # For tests that require Windows to run.
            features.add("system-windows")
        elif platform.system() == "Linux":
            features.add("system-linux")
        elif platform.system() in ["FreeBSD"]:
            features.add("system-freebsd")
        elif platform.system() == "NetBSD":
            features.add("system-netbsd")
        elif platform.system() == "AIX":
            features.add("system-aix")
        elif platform.system() == "SunOS":
            features.add("system-solaris")
        elif platform.system() == "OS/390":
            features.add("system-zos")

        # Native compilation: host arch == default triple arch
        # Both of these values should probably be in every site config (e.g. as
        # part of the standard header.  But currently they aren't)
        host_triple = getattr(config, "host_triple", None)
        target_triple = getattr(config, "target_triple", None)
        features.add("target=%s" % target_triple)
        if host_triple and host_triple == target_triple:
            features.add("native")

        # Sanitizers.
        sanitizers = getattr(config, "llvm_use_sanitizer", "")
        sanitizers = frozenset(x.lower() for x in sanitizers.split(";"))
        if "address" in sanitizers:
            features.add("asan")
        if "hwaddress" in sanitizers:
            features.add("hwasan")
        if "memory" in sanitizers or "memorywithorigins" in sanitizers:
            features.add("msan")
        if "undefined" in sanitizers:
            features.add("ubsan")

        have_zlib = getattr(config, "have_zlib", None)
        if have_zlib:
            features.add("zlib")
        have_zstd = getattr(config, "have_zstd", None)
        if have_zstd:
            features.add("zstd")

        if getattr(config, "reverse_iteration", None):
            features.add("reverse_iteration")

        # Check if we should run long running tests.
        long_tests = lit_config.params.get("run_long_tests", None)
        if lit.util.pythonize_bool(long_tests):
            features.add("long_tests")

        if target_triple:
            if re.match(r"^x86_64.*-apple", target_triple):
                features.add("x86_64-apple")
                host_cxx = getattr(config, "host_cxx", None)
                if "address" in sanitizers and self.get_clang_has_lsan(
                    host_cxx, target_triple
                ):
                    self.with_environment(
                        "ASAN_OPTIONS", "detect_leaks=1", append_path=True
                    )
            if re.match(r"^x86_64.*-linux", target_triple):
                features.add("x86_64-linux")
            if re.match(r"^i.86.*", target_triple):
                features.add("target-x86")
            elif re.match(r"^x86_64.*", target_triple):
                features.add("target-x86_64")
            elif re.match(r"^aarch64.*", target_triple):
                features.add("target-aarch64")
            elif re.match(r"^arm64.*", target_triple):
                features.add("target-aarch64")
            elif re.match(r"^arm.*", target_triple):
                features.add("target-arm")
            if re.match(r'^ppc64le.*-linux', target_triple):
                features.add('target=powerpc64le-linux')

        use_gmalloc = lit_config.params.get("use_gmalloc", None)
        if lit.util.pythonize_bool(use_gmalloc):
            # Allow use of an explicit path for gmalloc library.
            # Will default to '/usr/lib/libgmalloc.dylib' if not set.
            gmalloc_path_str = lit_config.params.get(
                "gmalloc_path", "/usr/lib/libgmalloc.dylib"
            )
            if gmalloc_path_str is not None:
                self.with_environment("DYLD_INSERT_LIBRARIES", gmalloc_path_str)

    def _find_git_windows_unix_tools(self, tools_needed):
        assert sys.platform == "win32"
        if sys.version_info.major >= 3:
            import winreg
        else:
            import _winreg as winreg

        # Search both the 64 and 32-bit hives, as well as HKLM + HKCU
        masks = [0, winreg.KEY_WOW64_64KEY]
        hives = [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]
        for mask, hive in itertools.product(masks, hives):
            try:
                with winreg.OpenKey(
                    hive, r"SOFTWARE\GitForWindows", 0, winreg.KEY_READ | mask
                ) as key:
                    install_root, _ = winreg.QueryValueEx(key, "InstallPath")

                    if not install_root:
                        continue
                    candidate_path = os.path.join(install_root, "usr", "bin")
                    if not lit.util.checkToolsPath(candidate_path, tools_needed):
                        continue

                    # We found it, stop enumerating.
                    return lit.util.to_string(candidate_path)
            except:
                continue

        return None

    def with_environment(self, variable, value, append_path=False):
        if append_path:
            # For paths, we should be able to take a list of them and process
            # all of them.
            paths_to_add = value
            if lit.util.is_string(paths_to_add):
                paths_to_add = [paths_to_add]

            def norm(x):
                return os.path.normcase(os.path.normpath(x))

            current_paths = self.config.environment.get(variable, None)
            if current_paths:
                current_paths = current_paths.split(os.path.pathsep)
                paths = [norm(p) for p in current_paths]
            else:
                paths = []

            # If we are passed a list [a b c], then iterating this list forwards
            # and adding each to the beginning would result in c b a.  So we
            # need to iterate in reverse to end up with the original ordering.
            for p in reversed(paths_to_add):
                # Move it to the front if it already exists, otherwise insert
                # it at the beginning.
                p = norm(p)
                try:
                    paths.remove(p)
                except ValueError:
                    pass
                paths = [p] + paths
            value = os.pathsep.join(paths)
        self.config.environment[variable] = value

    def with_system_environment(self, variables, append_path=False):
        if lit.util.is_string(variables):
            variables = [variables]
        for v in variables:
            value = os.environ.get(v)
            if value:
                self.with_environment(v, value, append_path)

    def clear_environment(self, variables):
        for name in variables:
            if name in self.config.environment:
                del self.config.environment[name]

    def get_process_output(self, command):
        try:
            cmd = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                env=self.config.environment,
            )
            stdout, stderr = cmd.communicate()
            stdout = lit.util.to_string(stdout)
            stderr = lit.util.to_string(stderr)
            return (stdout, stderr)
        except OSError:
            self.lit_config.fatal("Could not run process %s" % command)

    def feature_config(self, features):
        # Ask llvm-config about the specified feature.
        arguments = [x for (x, _) in features]
        config_path = os.path.join(self.config.llvm_tools_dir, "llvm-config")

        output, _ = self.get_process_output([config_path] + arguments)
        lines = output.split("\n")

        for (feature_line, (_, patterns)) in zip(lines, features):
            # We should have either a callable or a dictionary.  If it's a
            # dictionary, grep each key against the output and use the value if
            # it matches.  If it's a callable, it does the entire translation.
            if callable(patterns):
                features_to_add = patterns(feature_line)
                self.config.available_features.update(features_to_add)
            else:
                for (re_pattern, feature) in patterns.items():
                    if re.search(re_pattern, feature_line):
                        self.config.available_features.add(feature)

    # Note that when substituting %clang_cc1 also fill in the include directory
    # of the builtin headers. Those are part of even a freestanding
    # environment, but Clang relies on the driver to locate them.
    def get_clang_builtin_include_dir(self, clang):
        # FIXME: Rather than just getting the version, we should have clang
        # print out its resource dir here in an easy to scrape form.
        clang_dir, _ = self.get_process_output([clang, "-print-file-name=include"])

        if not clang_dir:
            print(clang)
            self.lit_config.fatal(
                "Couldn't find the include dir for Clang ('%s')" % clang
            )

        clang_dir = clang_dir.strip()
        if sys.platform in ["win32"] and not self.use_lit_shell:
            # Don't pass dosish path separator to msys bash.exe.
            clang_dir = clang_dir.replace("\\", "/")
        # Ensure the result is an ascii string, across Python2.5+ - Python3.
        return clang_dir

    # On macOS, LSan is only supported on clang versions 5 and higher
    def get_clang_has_lsan(self, clang, triple):
        if not clang:
            self.lit_config.warning(
                "config.host_cxx is unset but test suite is configured "
                "to use sanitizers."
            )
            return False

        clang_binary = clang.split()[0]
        version_string, _ = self.get_process_output([clang_binary, "--version"])
        if not "clang" in version_string:
            self.lit_config.warning(
                "compiler '%s' does not appear to be clang, " % clang_binary
                + "but test suite is configured to use sanitizers."
            )
            return False

        if re.match(r".*-linux", triple):
            return True

        if re.match(r"^x86_64.*-apple", triple):
            version_regex = re.search(
                r"version ([0-9]+)\.([0-9]+).([0-9]+)", version_string
            )
            major_version_number = int(version_regex.group(1))
            minor_version_number = int(version_regex.group(2))
            patch_version_number = int(version_regex.group(3))
            if "Apple LLVM" in version_string or "Apple clang" in version_string:
                # Apple clang doesn't yet support LSan
                return False
            return major_version_number >= 5

        return False

    def make_itanium_abi_triple(self, triple):
        m = re.match(r"(\w+)-(\w+)-(\w+)", triple)
        if not m:
            self.lit_config.fatal(
                "Could not turn '%s' into Itanium ABI triple" % triple
            )
        if m.group(3).lower() != "windows":
            # All non-windows triples use the Itanium ABI.
            return triple
        return m.group(1) + "-" + m.group(2) + "-" + m.group(3) + "-gnu"

    def make_msabi_triple(self, triple):
        m = re.match(r"(\w+)-(\w+)-(\w+)", triple)
        if not m:
            self.lit_config.fatal("Could not turn '%s' into MS ABI triple" % triple)
        isa = m.group(1).lower()
        vendor = m.group(2).lower()
        os = m.group(3).lower()
        if os == "windows" and re.match(r".*-msvc$", triple):
            # If the OS is windows and environment is msvc, we're done.
            return triple
        if isa.startswith("x86") or isa == "amd64" or re.match(r"i\d86", isa):
            # For x86 ISAs, adjust the OS.
            return isa + "-" + vendor + "-windows-msvc"
        # -msvc is not supported for non-x86 targets; use a default.
        return "i686-pc-windows-msvc"

    def add_tool_substitutions(self, tools, search_dirs=None):
        if not search_dirs:
            search_dirs = [self.config.llvm_tools_dir]

        if lit.util.is_string(search_dirs):
            search_dirs = [search_dirs]

        tools = [x if isinstance(x, ToolSubst) else ToolSubst(x) for x in tools]

        search_dirs = os.pathsep.join(search_dirs)
        substitutions = []

        for tool in tools:
            match = tool.resolve(self, search_dirs)

            # Either no match occurred, or there was an unresolved match that
            # is ignored.
            if not match:
                continue

            subst_key, tool_pipe, command = match

            # An unresolved match occurred that can't be ignored.  Fail without
            # adding any of the previously-discovered substitutions.
            if not command:
                return False

            substitutions.append((subst_key, tool_pipe + command))

        self.config.substitutions.extend(substitutions)
        return True

    def add_err_msg_substitutions(self):
        # Python's strerror may not supply the same message
        # as C++ std::error_code. One example of such a platform is
        # Visual Studio. errc_messages may be supplied which contains the error
        # messages for ENOENT, EISDIR, EINVAL and EACCES as a semi colon
        # separated string. LLVM testsuites can use get_errc_messages in cmake
        # to automatically get the messages and pass them into lit.
        errc_messages = getattr(self.config, "errc_messages", "")
        if len(errc_messages) != 0:
            (errc_enoent, errc_eisdir, errc_einval, errc_eacces) = errc_messages.split(
                ";"
            )
            self.config.substitutions.append(("%errc_ENOENT", "'" + errc_enoent + "'"))
            self.config.substitutions.append(("%errc_EISDIR", "'" + errc_eisdir + "'"))
            self.config.substitutions.append(("%errc_EINVAL", "'" + errc_einval + "'"))
            self.config.substitutions.append(("%errc_EACCES", "'" + errc_eacces + "'"))
        else:
            self.config.substitutions.append(
                ("%errc_ENOENT", "'" + os.strerror(errno.ENOENT) + "'")
            )
            self.config.substitutions.append(
                ("%errc_EISDIR", "'" + os.strerror(errno.EISDIR) + "'")
            )
            self.config.substitutions.append(
                ("%errc_EINVAL", "'" + os.strerror(errno.EINVAL) + "'")
            )
            self.config.substitutions.append(
                ("%errc_EACCES", "'" + os.strerror(errno.EACCES) + "'")
            )

    def use_default_substitutions(self):
        tool_patterns = [
            ToolSubst("FileCheck", unresolved="fatal"),
            # Handle these specially as they are strings searched for during
            # testing.
            ToolSubst(
                r"\| \bcount\b",
                command=FindTool("count"),
                verbatim=True,
                unresolved="fatal",
            ),
            ToolSubst(
                r"\| \bnot\b",
                command=FindTool("not"),
                verbatim=True,
                unresolved="fatal",
            ),
        ]

        self.config.substitutions.append(("%python", '"%s"' % (sys.executable)))

        self.add_tool_substitutions(tool_patterns, [self.config.llvm_tools_dir])

        self.add_err_msg_substitutions()

    def use_llvm_tool(
        self,
        name,
        search_env=None,
        required=False,
        quiet=False,
        search_paths=None,
        use_installed=False,
    ):
        """Find the executable program 'name', optionally using the specified
        environment variable as an override before searching the build directory
        and then optionally the configuration's PATH."""
        # If the override is specified in the environment, use it without
        # validation.
        tool = None
        if search_env:
            tool = self.config.environment.get(search_env)

        if not tool:
            if search_paths is None:
                search_paths = [self.config.llvm_tools_dir]
            # Use the specified search paths.
            path = os.pathsep.join(search_paths)
            tool = lit.util.which(name, path)

        if not tool and use_installed:
            # Otherwise look in the path, if enabled.
            tool = lit.util.which(name, self.config.environment["PATH"])

        if required and not tool:
            message = "couldn't find '{}' program".format(name)
            if search_env:
                message = message + ", try setting {} in your environment".format(
                    search_env
                )
            self.lit_config.fatal(message)

        if tool:
            tool = os.path.normpath(tool)
            if not self.lit_config.quiet and not quiet:
                self.lit_config.note("using {}: {}".format(name, tool))
        return tool

    def use_clang(
        self,
        additional_tool_dirs=[],
        additional_flags=[],
        required=True,
        use_installed=False,
    ):
        """Configure the test suite to be able to invoke clang.

        Sets up some environment variables important to clang, locates a
        just-built or optionally an installed clang, and add a set of standard
        substitutions useful to any test suite that makes use of clang.

        """
        # Clear some environment variables that might affect Clang.
        #
        # This first set of vars are read by Clang, but shouldn't affect tests
        # that aren't specifically looking for these features, or are required
        # simply to run the tests at all.
        #
        # FIXME: Should we have a tool that enforces this?

        # safe_env_vars = (
        #     'TMPDIR', 'TEMP', 'TMP', 'USERPROFILE', 'PWD',
        #     'MACOSX_DEPLOYMENT_TARGET', 'IPHONEOS_DEPLOYMENT_TARGET',
        #     'VCINSTALLDIR', 'VC100COMNTOOLS', 'VC90COMNTOOLS',
        #     'VC80COMNTOOLS')
        possibly_dangerous_env_vars = [
            "COMPILER_PATH",
            "RC_DEBUG_OPTIONS",
            "CINDEXTEST_PREAMBLE_FILE",
            "LIBRARY_PATH",
            "CPATH",
            "C_INCLUDE_PATH",
            "CPLUS_INCLUDE_PATH",
            "OBJC_INCLUDE_PATH",
            "OBJCPLUS_INCLUDE_PATH",
            "LIBCLANG_TIMING",
            "LIBCLANG_OBJTRACKING",
            "LIBCLANG_LOGGING",
            "LIBCLANG_BGPRIO_INDEX",
            "LIBCLANG_BGPRIO_EDIT",
            "LIBCLANG_NOTHREADS",
            "LIBCLANG_RESOURCE_USAGE",
            "LIBCLANG_CODE_COMPLETION_LOGGING",
        ]
        # Clang/Win32 may refer to %INCLUDE%. vsvarsall.bat sets it.
        if platform.system() != "Windows":
            possibly_dangerous_env_vars.append("INCLUDE")

        self.clear_environment(possibly_dangerous_env_vars)

        # Tweak the PATH to include the tools dir and the scripts dir.
        # Put Clang first to avoid LLVM from overriding out-of-tree clang
        # builds.
        exe_dir_props = [
            self.config.name.lower() + "_tools_dir",
            "clang_tools_dir",
            "llvm_tools_dir",
        ]
        paths = [
            getattr(self.config, pp)
            for pp in exe_dir_props
            if getattr(self.config, pp, None)
        ]
        paths = additional_tool_dirs + paths
        self.with_environment("PATH", paths, append_path=True)

        lib_dir_props = [
            self.config.name.lower() + "_libs_dir",
            "llvm_shlib_dir",
            "llvm_libs_dir",
        ]
        lib_paths = [
            getattr(self.config, pp)
            for pp in lib_dir_props
            if getattr(self.config, pp, None)
        ]

        self.with_environment("LD_LIBRARY_PATH", lib_paths, append_path=True)

        shl = getattr(self.config, "llvm_shlib_dir", None)
        pext = getattr(self.config, "llvm_plugin_ext", None)
        if shl:
            self.config.substitutions.append(("%llvmshlibdir", shl))
        if pext:
            self.config.substitutions.append(("%pluginext", pext))

        # Discover the 'clang' and 'clangcc' to use.
        self.config.clang = self.use_llvm_tool(
            "clang",
            search_env="CLANG",
            required=required,
            search_paths=paths,
            use_installed=use_installed,
        )
        if self.config.clang:
            self.config.available_features.add("clang")
            builtin_include_dir = self.get_clang_builtin_include_dir(self.config.clang)
            tool_substitutions = [
                ToolSubst(
                    "%clang", command=self.config.clang, extra_args=additional_flags
                ),
                ToolSubst(
                    "%clang_analyze_cc1",
                    command="%clang_cc1",
                    extra_args=["-analyze", "%analyze", "-setup-static-analyzer"]
                    + additional_flags,
                ),
                ToolSubst(
                    "%clang_cc1",
                    command=self.config.clang,
                    extra_args=[
                        "-cc1",
                        "-internal-isystem",
                        builtin_include_dir,
                        "-nostdsysteminc",
                    ]
                    + additional_flags,
                ),
                ToolSubst(
                    "%clang_cpp",
                    command=self.config.clang,
                    extra_args=["--driver-mode=cpp"] + additional_flags,
                ),
                ToolSubst(
                    "%clang_cl",
                    command=self.config.clang,
                    extra_args=["--driver-mode=cl"] + additional_flags,
                ),
                ToolSubst(
                    "%clang_dxc",
                    command=self.config.clang,
                    extra_args=["--driver-mode=dxc"] + additional_flags,
                ),
                ToolSubst(
                    "%clangxx",
                    command=self.config.clang,
                    extra_args=["--driver-mode=g++"] + additional_flags,
                ),
            ]
            self.add_tool_substitutions(tool_substitutions)
            self.config.substitutions.append(("%resource_dir", builtin_include_dir))

        self.config.substitutions.append(
            (
                "%itanium_abi_triple",
                self.make_itanium_abi_triple(self.config.target_triple),
            )
        )
        self.config.substitutions.append(
            ("%ms_abi_triple", self.make_msabi_triple(self.config.target_triple))
        )

        # The host triple might not be set, at least if we're compiling clang
        # from an already installed llvm.
        if self.config.host_triple and self.config.host_triple != "@LLVM_HOST_TRIPLE@":
            self.config.substitutions.append(
                (
                    "%target_itanium_abi_host_triple",
                    "--target=" + self.make_itanium_abi_triple(self.config.host_triple),
                )
            )
        else:
            self.config.substitutions.append(("%target_itanium_abi_host_triple", ""))

        # TODO: Many tests work across many language standards. Before
        # https://discourse.llvm.org/t/lit-run-a-run-line-multiple-times-with-different-replacements/64932
        # has a solution, provide substitutions to conveniently try every standard with LIT_CLANG_STD_GROUP.
        clang_std_group = int(os.environ.get("LIT_CLANG_STD_GROUP", "0"))
        clang_std_values = ("98", "11", "14", "17", "20", "2b")

        def add_std_cxx(s):
            t = s[8:]
            if t.endswith("-"):
                t += clang_std_values[-1]
            l = clang_std_values.index(t[0:2] if t[0:2] != "23" else "2b")
            h = clang_std_values.index(t[3:5])
            # Let LIT_CLANG_STD_GROUP=0 pick the highest value (likely the most relevant
            # standard).
            l = h - clang_std_group % (h - l + 1)
            self.config.substitutions.append((s, "-std=c++" + clang_std_values[l]))

        add_std_cxx("%std_cxx98-14")
        add_std_cxx("%std_cxx98-")
        add_std_cxx("%std_cxx11-14")
        add_std_cxx("%std_cxx11-")
        add_std_cxx("%std_cxx14-")
        add_std_cxx("%std_cxx17-20")
        add_std_cxx("%std_cxx17-")
        add_std_cxx("%std_cxx20-")
        add_std_cxx("%std_cxx23-")

        # FIXME: Find nicer way to prohibit this.
        def prefer(this, to):
            return '''\"*** Do not use '%s' in tests, use '%s'. ***\"''' % (to, this)

        self.config.substitutions.append((" clang ", prefer("%clang", "clang")))
        self.config.substitutions.append(
            (r" clang\+\+ ", prefer("%clangxx", "clang++"))
        )
        self.config.substitutions.append(
            (" clang-cc ", prefer("%clang_cc1", "clang-cc"))
        )
        self.config.substitutions.append(
            (" clang-cl ", prefer("%clang_cl", "clang-cl"))
        )
        self.config.substitutions.append(
            (
                " clang -cc1 -analyze ",
                prefer("%clang_analyze_cc1", "clang -cc1 -analyze"),
            )
        )
        self.config.substitutions.append(
            (" clang -cc1 ", prefer("%clang_cc1", "clang -cc1"))
        )
        self.config.substitutions.append(
            (" %clang-cc1 ", '''\"*** invalid substitution, use '%clang_cc1'. ***\"''')
        )
        self.config.substitutions.append(
            (" %clang-cpp ", '''\"*** invalid substitution, use '%clang_cpp'. ***\"''')
        )
        self.config.substitutions.append(
            (" %clang-cl ", '''\"*** invalid substitution, use '%clang_cl'. ***\"''')
        )

    def use_lld(self, additional_tool_dirs=[], required=True, use_installed=False):
        """Configure the test suite to be able to invoke lld.

        Sets up some environment variables important to lld, locates a
        just-built or optionally an installed lld, and add a set of standard
        substitutions useful to any test suite that makes use of lld.

        """

        # Tweak the PATH to include the tools dir and the scripts dir.
        exe_dir_props = [
            self.config.name.lower() + "_tools_dir",
            "lld_tools_dir",
            "llvm_tools_dir",
        ]
        paths = [
            getattr(self.config, pp)
            for pp in exe_dir_props
            if getattr(self.config, pp, None)
        ]
        paths = additional_tool_dirs + paths
        self.with_environment("PATH", paths, append_path=True)

        lib_dir_props = [
            self.config.name.lower() + "_libs_dir",
            "lld_libs_dir",
            "llvm_shlib_dir",
            "llvm_libs_dir",
        ]
        lib_paths = [
            getattr(self.config, pp)
            for pp in lib_dir_props
            if getattr(self.config, pp, None)
        ]

        self.with_environment("LD_LIBRARY_PATH", lib_paths, append_path=True)

        # Discover the LLD executables to use.

        ld_lld = self.use_llvm_tool(
            "ld.lld", required=required, search_paths=paths, use_installed=use_installed
        )
        lld_link = self.use_llvm_tool(
            "lld-link",
            required=required,
            search_paths=paths,
            use_installed=use_installed,
        )
        ld64_lld = self.use_llvm_tool(
            "ld64.lld",
            required=required,
            search_paths=paths,
            use_installed=use_installed,
        )
        wasm_ld = self.use_llvm_tool(
            "wasm-ld",
            required=required,
            search_paths=paths,
            use_installed=use_installed,
        )

        was_found = ld_lld and lld_link and ld64_lld and wasm_ld
        tool_substitutions = []
        if ld_lld:
            tool_substitutions.append(ToolSubst(r"ld\.lld", command=ld_lld))
            self.config.available_features.add("ld.lld")
        if lld_link:
            tool_substitutions.append(ToolSubst("lld-link", command=lld_link))
            self.config.available_features.add("lld-link")
        if ld64_lld:
            tool_substitutions.append(ToolSubst(r"ld64\.lld", command=ld64_lld))
            self.config.available_features.add("ld64.lld")
        if wasm_ld:
            tool_substitutions.append(ToolSubst("wasm-ld", command=wasm_ld))
            self.config.available_features.add("wasm-ld")
        self.add_tool_substitutions(tool_substitutions)

        return was_found
