blob: 198a6ee339d82ca45e0ab2e610208c8b275bbffb [file] [log] [blame]
# 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 []