# SPDX-License-Identifier: Apache-2.0 # Copyright 2012-2020 The Meson development team # Copyright © 2023-2025 Intel Corporation from __future__ import annotations import os import shutil import typing as T from . import coredata from . import mesonlib from . import mlog from .mesonlib import MachineChoice, Popen_safe, search_version, quote_arg, split_args from .programs import ExternalProgram def detect_gcovr(gcovr_exe: str = 'gcovr', min_version: str = '3.3', log: bool = False) \ -> T.Union[T.Tuple[None, None], T.Tuple[str, str]]: try: p, found = Popen_safe([gcovr_exe, '--version'])[0:2] except (FileNotFoundError, PermissionError): # Doesn't exist in PATH or isn't executable return None, None found = search_version(found) if p.returncode == 0 and mesonlib.version_compare(found, '>=' + min_version): if log: mlog.log('Found gcovr-{} at {}'.format(found, quote_arg(shutil.which(gcovr_exe)))) return gcovr_exe, found return None, None def detect_lcov(lcov_exe: str = 'lcov', log: bool = False) \ -> T.Union[T.Tuple[None, None], T.Tuple[str, str]]: try: p, found = Popen_safe([lcov_exe, '--version'])[0:2] except (FileNotFoundError, PermissionError): # Doesn't exist in PATH or isn't executable return None, None found = search_version(found) if p.returncode == 0 and found: if log: mlog.log('Found lcov-{} at {}'.format(found, quote_arg(shutil.which(lcov_exe)))) return lcov_exe, found return None, None def detect_llvm_cov(suffix: T.Optional[str] = None) -> T.Optional[str]: # If there's a known suffix or forced lack of suffix, use that if suffix is not None: if suffix == '': tool = 'llvm-cov' else: tool = f'llvm-cov-{suffix}' if shutil.which(tool) is not None: return tool else: # Otherwise guess in the dark tools = get_llvm_tool_names('llvm-cov') for tool in tools: if shutil.which(tool): return tool return None def compute_llvm_suffix(coredata: coredata.CoreData) -> T.Optional[str]: # Check to see if the user is trying to do coverage for either a C or C++ project compilers = coredata.compilers[MachineChoice.BUILD] cpp_compiler_is_clang = 'cpp' in compilers and compilers['cpp'].id == 'clang' c_compiler_is_clang = 'c' in compilers and compilers['c'].id == 'clang' # Extract first the C++ compiler if available. If it's a Clang of some kind, compute the suffix if possible if cpp_compiler_is_clang: suffix = compilers['cpp'].version.split('.')[0] return suffix # Then the C compiler, again checking if it's some kind of Clang and computing the suffix if c_compiler_is_clang: suffix = compilers['c'].version.split('.')[0] return suffix # Neither compiler is a Clang, or no compilers are for C or C++ return None def detect_lcov_genhtml(lcov_exe: str = 'lcov', genhtml_exe: str = 'genhtml') \ -> T.Tuple[str, T.Optional[str], str]: lcov_exe, lcov_version = detect_lcov(lcov_exe) if shutil.which(genhtml_exe) is None: genhtml_exe = None return lcov_exe, lcov_version, genhtml_exe def find_coverage_tools(coredata: coredata.CoreData) -> T.Tuple[T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str]]: gcovr_exe, gcovr_version = detect_gcovr() llvm_cov_exe = detect_llvm_cov(compute_llvm_suffix(coredata)) # Some platforms may provide versioned clang but only non-versioned llvm utils if llvm_cov_exe is None: llvm_cov_exe = detect_llvm_cov('') lcov_exe, lcov_version, genhtml_exe = detect_lcov_genhtml() return gcovr_exe, gcovr_version, lcov_exe, lcov_version, genhtml_exe, llvm_cov_exe def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.Optional[T.List[str]]: r = detect_ninja_command_and_version(version, log) return r[0] if r else None def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> T.Optional[T.Tuple[T.List[str], str]]: env_ninja = os.environ.get('NINJA', None) for n in [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']: prog = ExternalProgram(n, silent=True) if not prog.found(): continue try: p, found = Popen_safe(prog.command + ['--version'])[0:2] except (FileNotFoundError, PermissionError): # Doesn't exist in PATH or isn't executable continue found = found.strip() # Perhaps we should add a way for the caller to know the failure mode # (not found or too old) if p.returncode == 0 and mesonlib.version_compare(found, '>=' + version): if log: name = os.path.basename(n) if name.endswith('-' + found): name = name[0:-1 - len(found)] if name == 'ninja-build': name = 'ninja' if name == 'samu': name = 'samurai' mlog.log('Found {}-{} at {}'.format(name, found, ' '.join([quote_arg(x) for x in prog.command]))) return (prog.command, found) return None def get_llvm_tool_names(tool: str) -> T.List[str]: # Ordered list of possible suffixes of LLVM executables to try. Start with # base, then try newest back to oldest (3.5 is arbitrary), and finally the # devel version. Please note that the development snapshot in Debian does # not have a distinct name. Do not move it to the beginning of the list # unless it becomes a stable release. suffixes = [ '', # base (no suffix) '-21.1', '21.1', '-21', '21', '-20.1', '20.1', '-20', '20', '-19.1', '19.1', '-19', '19', '-18.1', '18.1', '-18', '18', '-17', '17', '-16', '16', '-15', '15', '-14', '14', '-13', '13', '-12', '12', '-11', '11', '-10', '10', '-9', '90', '-8', '80', '-7', '70', '-6.0', '60', '-5.0', '50', '-4.0', '40', '-3.9', '39', '-3.8', '38', '-3.7', '37', '-3.6', '36', '-3.5', '35', '-20', # Debian development snapshot '-devel', # FreeBSD development snapshot ] names: T.List[str] = [] for suffix in suffixes: names.append(tool + suffix) return names def detect_scanbuild() -> T.List[str]: """ Look for scan-build binary on build platform First, if a SCANBUILD env variable has been provided, give it precedence on all platforms. For most platforms, scan-build is found is the PATH contains a binary named "scan-build". However, some distribution's package manager (FreeBSD) don't. For those, loop through a list of candidates to see if one is available. Return: a single-element list of the found scan-build binary ready to be passed to Popen() """ exelist: T.List[str] = [] if 'SCANBUILD' in os.environ: exelist = split_args(os.environ['SCANBUILD']) else: tools = get_llvm_tool_names('scan-build') for tool in tools: which = shutil.which(tool) if which is not None: exelist = [which] break if exelist: tool = exelist[0] if os.path.isfile(tool) and os.access(tool, os.X_OK): return [tool] return [] def detect_clangformat() -> T.List[str]: """ Look for clang-format binary on build platform Do the same thing as detect_scanbuild to find clang-format except it currently does not check the environment variable. Return: a single-element list of the found clang-format binary ready to be passed to Popen() """ tools = get_llvm_tool_names('clang-format') for tool in tools: path = shutil.which(tool) if path is not None: return [path] return [] def detect_clangtidy() -> T.List[str]: """ Look for clang-tidy binary on build platform Return: a single-element list of the found clang-tidy binary ready to be passed to Popen() """ tools = get_llvm_tool_names('clang-tidy') for tool in tools: path = shutil.which(tool) if path is not None: return [path] return [] def detect_clangapply() -> T.List[str]: """ Look for clang-apply-replacements binary on build platform Return: a single-element list of the found clang-apply-replacements binary ready to be passed to Popen() """ tools = get_llvm_tool_names('clang-apply-replacements') for tool in tools: path = shutil.which(tool) if path is not None: return [path] return []