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 def user_is_root(): # os.getuid() is not available on all platforms try: if os.getuid() == 0: return True except: pass return 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("host=%s" % host_triple) 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') if not user_is_root(): features.add("non-root-user") 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)) # There will be no default target triple if one was not specifically # set, and the host's architecture is not an enabled target. if self.config.target_triple: 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)) ) else: if not self.lit_config.quiet: self.lit_config.note( "No default target triple was found, some tests may fail as a result." ) self.config.substitutions.append(("%itanium_abi_triple", "")) self.config.substitutions.append(("%ms_abi_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