diff --git a/docs/markdown/Dependencies.md b/docs/markdown/Dependencies.md index d91582523d44..346e7402e14b 100644 --- a/docs/markdown/Dependencies.md +++ b/docs/markdown/Dependencies.md @@ -602,6 +602,41 @@ llvm_dep = dependency('llvm', version : ['>= 8', '< 9']) llvm_link = find_program(llvm_dep.get_variable(configtool: 'bindir') / 'llvm-link') ``` +## Clang + +*(since 1.6.0)* + +Meson has native support for Clang, as well as support for using CMake to find Clang. +Because of the tight coupling between Clang and LLVM, the Clang dependency has a +specific argument to select the LLVM to use, or an internal version will be used +(When using the system based finder). This argument is unused with the CMake finder: + +```meson +llvm = dependency('llvm', version : ['>= 16', '< 17']) +clang = dependency('clang', version : ['>= 16', '< 17'], llvm : llvm, method : 'system') +``` + +Both libclang (the C interface) and the C++ interfaces are supported via the +`language` keyword. The default is to search for the `C` interface. + +If the `language` is `c`, then `libclang` will be searched for. This may be +built static or shared, and is a Clang configuration option. + +Otherwise, if the dependency may be shared, `clang-cpp` will be searched for +before loose clang libraries. It is always considered to have all of the modules +included. + +`method` may be `auto`, `system`, or `cmake`. + +### Modules + +Clang modules are supported, and must be passed in the format `clangBasic`, with +proper capitalization and the `clang` prepended. + +```meson +clang = dependency('clang', static : true, modules : ['clangBasic', 'clangIndex']) +``` + ## MPI *(added 0.42.0)* diff --git a/docs/markdown/snippets/clang_dependency.md b/docs/markdown/snippets/clang_dependency.md new file mode 100644 index 000000000000..467b57619ac6 --- /dev/null +++ b/docs/markdown/snippets/clang_dependency.md @@ -0,0 +1,5 @@ +## A Clang dependency + +This helps to simplify the use of libclang, removing the need to try cmake and +then falling back to not cmake. It also transparently handles the issues +associated with different paths to find Clang on different OSes. diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py index 89d2285ba3a6..eb54eafe42a7 100644 --- a/mesonbuild/dependencies/__init__.py +++ b/mesonbuild/dependencies/__init__.py @@ -188,6 +188,7 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T. # - a string naming the submodule that should be imported from `mesonbuild.dependencies` to populate the dependency packages.defaults.update({ # From dev: + 'clang': 'dev', 'gtest': 'dev', 'gmock': 'dev', 'llvm': 'dev', @@ -246,6 +247,7 @@ def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T. 'qt6': 'qt', }) _packages_accept_language.update({ + 'clang', 'hdf5', 'mpi', 'netcdf', diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py index de85516feb64..b20aca1ee6ff 100644 --- a/mesonbuild/dependencies/dev.py +++ b/mesonbuild/dependencies/dev.py @@ -506,6 +506,172 @@ def _original_module_name(self, module: str) -> str: return module +class ClangSystemDependency(SystemDependency): + + def __init__(self, name: str, env: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: + language = kwargs.get('language', language) + if language not in {None, 'c', 'cpp'}: + raise DependencyException('Clang only provides C and C++ language support') + + super().__init__(name, env, kwargs, language) + self.feature_since = ('1.6.0', '') + self.module_details: T.List[str] = [] + + # Clang may be installed a number of different ways: + # + # 1. Clang is installed directly in a common search path + # 2. Clang is installed alongside LLVM in a separate path to allow multiple versions + # to be co-installed. (Debian and Gentoo do this) + # 3. LLVM and Clang are installed in separate, default search paths. (NixOS does this) + # + # In order to accommodate all three of these we need to search both in + # the LLVM directory and outside of it. Start with the LLVM dir to avoid + # a situation where there is Clang next to LLVM and a different one in a + # common path + # + # Try to handle the combinations of CMake and config-tool LLVM with this + # method, even though it probably doesn't make sense to use the system + # finder for Clang with CMake LLVM + llvm = T.cast('T.Optional[ExternalDependency]', kwargs.get('llvm')) + if llvm is not None: + if not llvm.found(): + mlog.debug('Passed LLVM was not found, treating Clang as not found') + return + if self.version_reqs and not mesonlib.version_compare_many(llvm.version, self.version_reqs): + mlog.debug('Passed LLVMs version does not match the version required for Clang, treating it as not found') + return + self.ext_deps.append(llvm) + else: + if not self._add_sub_dependency( + llvm_factory( + env, self.for_machine, {'required': False, 'version': kwargs.get('version'), 'method': 'config-tool'})): + return + llvm = T.cast('ExternalDependency', self.ext_deps[0]) + # Clang and LLVM need to have the same version + self.version = llvm.version + + # libclang-cpp.so does not require modules, but there is no static equivalent + modules = stringlistify(extract_as_list(kwargs, 'modules')) + if not modules and language == 'cpp': + mlog.warning('Clang C++ dependency without modules works correctly for dynamically linked Clang, ' + 'but will fail to find a statically linked Clang', once=True, fatal=False) + + dirs: T.List[T.List[str]] = [[llvm.get_variable(configtool='libdir', cmake='LLVM_LIBRARY_DIR')], []] + + # Clang provides up to two interfaces for C++ code, and only one for C + # + # For C++ you can use libclang-cpp.so, or you can use loose static + # archives (This is just like LLVM). + # + # For C you use libclang which may be built static or shared, depending + # on configuration. + if not self.static or language == 'c': + if language == 'cpp': + # Use strict libtypes for C++ since we can fall through to + # individual libs if we can't find what + libtype = mesonlib.LibType.SHARED + libname = 'clang-cpp' + else: + libtype = mesonlib.LibType.PREFER_STATIC if self.static else mesonlib.LibType.PREFER_SHARED + libname = 'clang' + + for search in dirs: + lib = self.clib_compiler.find_library(libname, env, search, libtype=libtype) + if lib: + # Version.h is a C++ header, and this will fail if we look + # for clang-c. The inc is just the basic + version = self.clib_compiler.get_define('CLANG_VERSION', '#include ', env, lib, self.ext_deps)[0] + if not version: + mlog.debug(f'Could not find Clang in {search}, Becuase Version header was not found') + continue + + if not self.version_reqs or mesonlib.version_compare_many(version, self.version_reqs): + self.version = version + self.link_args = lib + self.is_found = True + return + + # If we don't have modules, or we're looking for C we're done, it's not going to find anything anyway + if not modules or language != 'cpp': + return + + opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules')) + + libtype = mesonlib.LibType.PREFER_STATIC if self.static else mesonlib.LibType.PREFER_SHARED + + for search in dirs: + self.module_details.clear() + libs: T.List[str] = [] + for m in modules: + lib = self.clib_compiler.find_library(m, env, search, libtype) + if lib: + libs.extend(lib) + self.module_details.append(m) + else: + self.module_details.append(f'{m} (missing)') + # Intentionally do not break here so that we can get an + # accurate count of missing modules + if len(modules) != len(libs): + mlog.debug(f'Could not find Clang in {search}, ' + f'because of missing modules: {self.module_details}') + continue + + for m in opt_modules: + lib = self.clib_compiler.find_library(m, env, search, libtype) + if lib: + libs.extend(lib) + self.module_details.append(m) + else: + self.module_details.append(f'{m} (missing but optional)') + + version = self.clib_compiler.get_define('CLANG_VERSION', '#include ', env, libs, self.ext_deps)[0] + if not version: + mlog.debug(f'Could not find Clang in {search}, Becuase Version header was not found') + continue + + if not self.version_reqs or mesonlib.version_compare_many(version, self.version_reqs): + self.version = version + self.link_args = libs + self.is_found = True + return + + mlog.debug(f'Could not find Clang in {search}, because of version mismatch, ' + f'required {", ".join(self.version_reqs)}, version: {version}') + + def log_details(self) -> str: + if self.module_details: + return 'modules: ' + ', '.join(self.module_details) + return '' + + +class ClangCMakeDependency(CMakeDependency): + + def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None, + force_use_global_compilers: bool = False) -> None: + language = kwargs.get('language', language) + + # libclang-cpp.so does not require modules, but there is no static equivalent + if not kwargs.get('modules') and language == 'cpp': + mlog.warning('Clang C++ dependency without modules works correctly for dynamically linked Clang, ' + 'but will fail to find a statically linked Clang', once=True, fatal=False) + + # There are no loose libs for the C api, only libclang + if language != 'cpp': + kwargs['modules'] = ['libclang'] + elif not kwargs.get('static', False): + # XXX: We really need to try twice here, once for clang-cpp and once + # for individual libs. We're probably going to need a custom + # factory… + kwargs['modules'] = ['clang-cpp'] + + # The C compiler is required for C++ mode, otherwise it will fail. + # Setting this option will add the C compielr if it's enabled + if language == 'cpp': + force_use_global_compilers = True + + super().__init__(name, environment, kwargs, language, force_use_global_compilers) + + class ValgrindDependency(PkgConfigDependency): ''' Consumers of Valgrind usually only need the compile args and do not want to @@ -699,6 +865,13 @@ def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW): packages['jdk'] = JDKSystemDependency +packages['clang'] = DependencyFactory( + 'clang', + [DependencyMethods.CMAKE, DependencyMethods.SYSTEM], + cmake_class=ClangCMakeDependency, + cmake_name='Clang', + system_class=ClangSystemDependency, +) packages['llvm'] = llvm_factory = DependencyFactory( 'LLVM', diff --git a/test cases/frameworks/38 clang/main.c b/test cases/frameworks/38 clang/main.c new file mode 100644 index 000000000000..804a0683077d --- /dev/null +++ b/test cases/frameworks/38 clang/main.c @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright © 2024 Intel Corporation + */ + +#include + +#include + +int main(int argc, char * argv[]) { + if (argc < 2) { + fprintf(stderr, "At least one argument is required!\n"); + return 1; + } + + const char * file = argv[1]; + + CXIndex index = clang_createIndex(0, 0); + CXTranslationUnit unit = clang_parseTranslationUnit( + index, + file, NULL, 0, + NULL, 0, + CXTranslationUnit_None); + + if (unit == NULL) { + return 1; + } + + clang_disposeTranslationUnit(unit); + clang_disposeIndex(index); + + return 0; +} diff --git a/test cases/frameworks/38 clang/main.cpp b/test cases/frameworks/38 clang/main.cpp new file mode 100644 index 000000000000..7f79f1ad7a00 --- /dev/null +++ b/test cases/frameworks/38 clang/main.cpp @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2010, Larry Olson + * Copyright © 2024 Intel Corporation + * + * Taken from: https://github.com/loarabia/Clang-tutorial/blob/master/CItutorial2.cpp + * + * With updates for LLVM and Clang 18 + */ + +#include "llvm/Config/llvm-config.h" + +#if LLVM_VERSION_MAJOR >= 18 +#include "llvm/TargetParser/Host.h" +#else +#include "llvm/Support/Host.h" +#endif +#include "llvm/ADT/IntrusiveRefCntPtr.h" + +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Frontend/TextDiagnosticPrinter.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Basic/TargetOptions.h" +#include "clang/Basic/TargetInfo.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Basic/Diagnostic.h" + +#include + +/****************************************************************************** + * + *****************************************************************************/ +int main(int argc, const char * argv[]) +{ + using clang::CompilerInstance; + using clang::TargetOptions; + using clang::TargetInfo; +#if LLVM_VERSION_MAJOR >= 18 + using clang::FileEntryRef; +#else + using clang::FileEntry; +#endif + using clang::Token; + using clang::DiagnosticOptions; + using clang::TextDiagnosticPrinter; + + if (argc != 2) { + std::cerr << "Need exactly 2 arguments." << std::endl; + return 1; + } + + CompilerInstance ci; + DiagnosticOptions diagnosticOptions; + ci.createDiagnostics(); + + std::shared_ptr pto = std::make_shared(); + pto->Triple = llvm::sys::getDefaultTargetTriple(); + TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), pto); + ci.setTarget(pti); + + ci.createFileManager(); + ci.createSourceManager(ci.getFileManager()); + ci.createPreprocessor(clang::TU_Complete); + + +#if LLVM_VERSION_MAJOR >= 18 + const FileEntryRef pFile = ci.getFileManager().getFileRef(argv[1]).get(); +#else + const FileEntry *pFile = ci.getFileManager().getFile(argv[1]).get(); +#endif + ci.getSourceManager().setMainFileID( ci.getSourceManager().createFileID( pFile, clang::SourceLocation(), clang::SrcMgr::C_User)); + ci.getPreprocessor().EnterMainSourceFile(); + ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), + &ci.getPreprocessor()); + Token tok; + bool err; + do { + ci.getPreprocessor().Lex(tok); + err = ci.getDiagnostics().hasErrorOccurred(); + if (err) break; + ci.getPreprocessor().DumpToken(tok); + std::cerr << std::endl; + } while ( tok.isNot(clang::tok::eof)); + ci.getDiagnosticClient().EndSourceFile(); + + return err ? 1 : 0; +} diff --git a/test cases/frameworks/38 clang/meson.build b/test cases/frameworks/38 clang/meson.build new file mode 100644 index 000000000000..a0d5be2af721 --- /dev/null +++ b/test cases/frameworks/38 clang/meson.build @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2024 Intel Corporation + +# C and C++ for are needed for cmake always +project('clangtest', 'c', 'cpp', default_options : ['c_std=c99', 'cpp_std=c++17']) + +method = get_option('method') +static = get_option('link-static') +language = get_option('language') + +test_file = files('test.cpp') + +if language == 'c' + dep_clang_c = dependency('clang', method : method, static : static, language : 'c', required : false) + if not dep_clang_c.found() + error('MESON_SKIP_TEST Could not find Clang for C language') + endif + exe = executable('parser', 'main.c', dependencies : dep_clang_c) + test('C API', exe, args : [test_file]) +else + modules_to_find = [ + 'clangAST', + 'clangASTMatchers', + 'clangAnalysis', + 'clangBasic', + 'clangDriver', + 'clangEdit', + 'clangFrontend', + 'clangLex', + 'clangParse', + 'clangSema', + 'clangSerialization', + 'clangSupport', + ] + + dep_clang_cpp = dependency('clang', modules : modules_to_find, method : method, static : static, language : 'cpp', required : false) + if not dep_clang_cpp.found() + error('MESON_SKIP_TEST Could not find Clang for C++ language') + endif + exe = executable('cpp-parser', 'main.cpp', dependencies : dep_clang_cpp) + test('C++ API', exe, args : [test_file]) +endif diff --git a/test cases/frameworks/38 clang/meson.options b/test cases/frameworks/38 clang/meson.options new file mode 100644 index 000000000000..023933888934 --- /dev/null +++ b/test cases/frameworks/38 clang/meson.options @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2024 Intel Corporation + +option( + 'method', + type : 'combo', + choices : ['system', 'cmake'] +) +option( + 'link-static', + type : 'boolean', + value : false, +) +option( + 'language', + type : 'combo', + choices : ['c', 'cpp'], + value : 'c', +) diff --git a/test cases/frameworks/38 clang/test.cpp b/test cases/frameworks/38 clang/test.cpp new file mode 100644 index 000000000000..396ba0757cfd --- /dev/null +++ b/test cases/frameworks/38 clang/test.cpp @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright © 2024 Intel Corporation + */ + +int func() { + return 1; +} diff --git a/test cases/frameworks/38 clang/test.json b/test cases/frameworks/38 clang/test.json new file mode 100644 index 000000000000..133aace5aaee --- /dev/null +++ b/test cases/frameworks/38 clang/test.json @@ -0,0 +1,30 @@ +{ + "matrix": { + "options": { + "method": [ + { "val": "system", "expect_skip_on_jobname": ["msys2-gccx64ninja", "msys2-gccx86ninja", "azure-vc2019x64vs", "azure-vc2019x64ninja"] }, + { "val": "cmake", "expect_skip_on_jobname": ["msys2-gccx64ninja", "msys2-gccx86ninja", "azure-vc2019x64vs", "azure-vc2019x64ninja"] } + ], + "link-static": [ + { + "val": true, + "expect_skip_on_jobname": [ + "linux-arch-gcc", "linux-arch-gcc-pypy", "linux-opensuse-gcc", + "linux-fedora-gcc", "linux-gentoo-gcc" + ] + }, + { "val": false } + ], + "language": [ + { "val": "c" }, + { "val": "cpp" } + ] + }, + "exclude": [ + { "link-static": true, "language": "c" } + ] + }, + "tools": { + "cmake": ">=3.11" + } +}